mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-06 13:39:07 +02:00
# Conflicts: # client/ios/NetBirdSDK/login.go # client/server/server.go # shared/management/proto/management.pb.go
280 lines
9.0 KiB
Go
280 lines
9.0 KiB
Go
package android
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
"github.com/netbirdio/netbird/client/internal/auth"
|
|
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
|
"github.com/netbirdio/netbird/client/mdm"
|
|
"github.com/netbirdio/netbird/client/mobile"
|
|
"github.com/netbirdio/netbird/client/system"
|
|
)
|
|
|
|
// SSOListener is async listener for mobile framework
|
|
type SSOListener interface {
|
|
OnSuccess(bool)
|
|
OnError(error)
|
|
}
|
|
|
|
// ErrListener is async listener for mobile framework
|
|
type ErrListener interface {
|
|
OnSuccess()
|
|
OnError(error)
|
|
}
|
|
|
|
// URLOpener it is a callback interface. The Open function will be triggered if
|
|
// the backend want to show an url for the user
|
|
type URLOpener interface {
|
|
Open(url string, userCode string)
|
|
OnLoginSuccess()
|
|
}
|
|
|
|
// Auth can register or login new client
|
|
type Auth struct {
|
|
ctx context.Context
|
|
config *profilemanager.Config
|
|
cfgPath string
|
|
}
|
|
|
|
// NewAuth instantiate Auth struct and validate the management URL
|
|
//
|
|
// The configuration at cfgPath is reused when one is already there, and only created when it is
|
|
// not. Building a fresh in-memory config unconditionally gives the client a new WireGuard key on
|
|
// every call: the peer registers under that key, the key is written out, and any peer registered by
|
|
// an earlier call is orphaned on the server. It also breaks a client that enrols and then runs from
|
|
// the persisted config, because the identity it registered is not the one it runs with — the
|
|
// management stream rejects it with "no peer auth method provided".
|
|
//
|
|
// Auth is constructed under the active MDM policy: the policy is overlaid on
|
|
// the resolved config so the login runs against the enforced values, while
|
|
// the persisted config keeps the caller-supplied ones; a caller-supplied
|
|
// management URL is ignored while MDM manages that key. A nil fetcher
|
|
// disables MDM enforcement.
|
|
func NewAuth(cfgPath string, mgmURL string, fetcher PolicyFetcher) (*Auth, error) {
|
|
policy := loaderFor(fetcher).Load()
|
|
inputCfg := profilemanager.ConfigInput{ConfigPath: cfgPath}
|
|
if _, managed := policy.GetString(mdm.KeyManagementURL); !managed {
|
|
inputCfg.ManagementURL = mgmURL
|
|
}
|
|
|
|
cfg, err := profilemanager.UpdateOrCreateConfig(inputCfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cfg.ApplyMDMPolicy(policy)
|
|
|
|
return &Auth{
|
|
ctx: context.Background(),
|
|
config: cfg,
|
|
cfgPath: cfgPath,
|
|
}, nil
|
|
}
|
|
|
|
// NewAuthWithConfig instantiate Auth based on existing config. cfgPath is the
|
|
// file the config was loaded from; it identifies the profile whose account email
|
|
// backs the login_hint.
|
|
func NewAuthWithConfig(ctx context.Context, config *profilemanager.Config, cfgPath string) *Auth {
|
|
return &Auth{
|
|
ctx: ctx,
|
|
config: config,
|
|
cfgPath: cfgPath,
|
|
}
|
|
}
|
|
|
|
// SaveConfigIfSSOSupported reports whether the management server supports SSO; the config is already persisted by NewAuth.
|
|
func (a *Auth) SaveConfigIfSSOSupported(listener SSOListener) {
|
|
go func() {
|
|
sso, err := a.saveConfigIfSSOSupported()
|
|
if err != nil {
|
|
listener.OnError(err)
|
|
} else {
|
|
listener.OnSuccess(sso)
|
|
}
|
|
}()
|
|
}
|
|
|
|
func (a *Auth) saveConfigIfSSOSupported() (bool, error) {
|
|
authClient, err := auth.NewAuth(a.ctx, a.config.PrivateKey, a.config.ManagementURL, a.config)
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to create auth client: %v", err)
|
|
}
|
|
defer authClient.Close()
|
|
|
|
supportsSSO, err := authClient.IsSSOSupported(a.ctx)
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to check SSO support: %v", err)
|
|
}
|
|
|
|
return supportsSSO, nil
|
|
}
|
|
|
|
// LoginWithSetupKeyAndSaveConfig registers the peer with the setup key; the config is already persisted by NewAuth.
|
|
func (a *Auth) LoginWithSetupKeyAndSaveConfig(resultListener ErrListener, setupKey string, deviceName string) {
|
|
go func() {
|
|
err := a.loginWithSetupKeyAndSaveConfig(setupKey, deviceName)
|
|
if err != nil {
|
|
resultListener.OnError(err)
|
|
} else {
|
|
resultListener.OnSuccess()
|
|
}
|
|
}()
|
|
}
|
|
|
|
func (a *Auth) loginWithSetupKeyAndSaveConfig(setupKey string, deviceName string) error {
|
|
authClient, err := auth.NewAuth(a.ctx, a.config.PrivateKey, a.config.ManagementURL, a.config)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create auth client: %v", err)
|
|
}
|
|
defer authClient.Close()
|
|
|
|
//nolint
|
|
ctxWithValues := context.WithValue(a.ctx, system.DeviceNameCtxKey, deviceName)
|
|
err, _ = authClient.Login(ctxWithValues, setupKey, "")
|
|
if err != nil {
|
|
return fmt.Errorf("login failed: %v", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Login try register the client on the server
|
|
func (a *Auth) Login(resultListener ErrListener, urlOpener URLOpener, isAndroidTV bool) {
|
|
go func() {
|
|
err := a.login(urlOpener, isAndroidTV)
|
|
if err != nil {
|
|
resultListener.OnError(err)
|
|
} else {
|
|
resultListener.OnSuccess()
|
|
}
|
|
}()
|
|
}
|
|
|
|
func (a *Auth) login(urlOpener URLOpener, isAndroidTV bool) error {
|
|
authClient, err := auth.NewAuth(a.ctx, a.config.PrivateKey, a.config.ManagementURL, a.config)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create auth client: %v", err)
|
|
}
|
|
defer authClient.Close()
|
|
|
|
// check if we need to generate JWT token
|
|
needsLogin, err := authClient.IsLoginRequired(a.ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to check login requirement: %v", err)
|
|
}
|
|
|
|
jwtToken := ""
|
|
email := ""
|
|
if needsLogin {
|
|
tokenInfo, err := a.foregroundGetTokenInfo(authClient, urlOpener, isAndroidTV)
|
|
if err != nil {
|
|
return fmt.Errorf("interactive sso login failed: %v", err)
|
|
}
|
|
jwtToken = tokenInfo.GetTokenToUse()
|
|
email = tokenInfo.Email
|
|
}
|
|
|
|
err, _ = authClient.Login(a.ctx, "", jwtToken)
|
|
if err != nil {
|
|
return fmt.Errorf("login failed: %v", err)
|
|
}
|
|
|
|
// Stored after Login, not before: a rejected token must not leave a hint
|
|
// pointing at an account that cannot be used.
|
|
if email != "" && a.cfgPath != "" {
|
|
if err := mobile.WriteProfileEmail(a.cfgPath, email); err != nil {
|
|
log.Warnf("failed to store profile account email: %v", err)
|
|
}
|
|
}
|
|
|
|
go urlOpener.OnLoginSuccess()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *Auth) foregroundGetTokenInfo(authClient *auth.Auth, urlOpener URLOpener, isAndroidTV bool) (*auth.TokenInfo, error) {
|
|
return a.foregroundGetTokenInfoFlow(authClient, urlOpener, isAndroidTV, false)
|
|
}
|
|
|
|
// foregroundGetTokenInfoFlow runs the interactive flow. sessionExtend tells the
|
|
// server the token will renew this peer's session rather than log a peer in, so
|
|
// it can rule out a silent authorization the IdP could answer from an unrelated
|
|
// account. See PKCEAuthorizationFlowRequest.
|
|
func (a *Auth) foregroundGetTokenInfoFlow(authClient *auth.Auth, urlOpener URLOpener, isAndroidTV bool, sessionExtend bool) (*auth.TokenInfo, error) {
|
|
hint := profileLoginHint(a.cfgPath)
|
|
|
|
oAuthFlow, err := authClient.GetOAuthFlow(a.ctx, isAndroidTV, sessionExtend, hint)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get OAuth flow: %v", err)
|
|
}
|
|
|
|
tokenInfo, err := runOAuthFlow(a.ctx, oAuthFlow, urlOpener, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if tokenInfo.MatchesAccount(hint) {
|
|
return tokenInfo, nil
|
|
}
|
|
|
|
// The IdP answered from a session belonging to another account. Retrying is
|
|
// what makes this recoverable: on a peer already registered the server would
|
|
// reject the token, and on a fresh one it would silently register the peer
|
|
// under the wrong account and bind the profile to it.
|
|
log.Infof("login returned an account other than the one this profile is bound to, retrying with an account prompt")
|
|
retryFlow := auth.RetryFlowForAccount(oAuthFlow)
|
|
if retryFlow == nil {
|
|
return tokenInfo, nil
|
|
}
|
|
|
|
retryToken, err := runOAuthFlow(a.ctx, retryFlow, urlOpener, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !retryToken.MatchesAccount(hint) {
|
|
log.Warnf("login still returned a different account after the prompt, continuing with it")
|
|
}
|
|
|
|
return retryToken, nil
|
|
}
|
|
|
|
// profileLoginHint returns the stored account email for the profile at cfgPath.
|
|
// An empty hint is deliberate, not a fallback: a fresh profile leaves the
|
|
// choice to the IdP. Switching accounts is done by switching or removing
|
|
// profiles, not by logging out — logout keeps the email.
|
|
func profileLoginHint(cfgPath string) string {
|
|
if cfgPath == "" {
|
|
return ""
|
|
}
|
|
return mobile.ReadProfileEmail(cfgPath)
|
|
}
|
|
|
|
// runOAuthFlow drives an already acquired OAuth flow to a token: requests the
|
|
// flow info, presents the verification URL through the opener and waits for
|
|
// the browser round-trip. Open is called synchronously — it is what marks the
|
|
// surface as opened on the client side, and a fast token's OnLoginSuccess is
|
|
// a no-op until it has, so the dismissal would be dropped rather than
|
|
// delayed. Openers must therefore not block: they post their UI work and
|
|
// return. onWaiting, when set, runs after the URL is shown, right before the
|
|
// blocking wait.
|
|
func runOAuthFlow(ctx context.Context, flow auth.OAuthFlow, urlOpener URLOpener, onWaiting func()) (*auth.TokenInfo, error) {
|
|
flowInfo, err := flow.RequestAuthInfo(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("request auth info: %w", err)
|
|
}
|
|
|
|
urlOpener.Open(flowInfo.VerificationURIComplete, flowInfo.UserCode)
|
|
|
|
if onWaiting != nil {
|
|
onWaiting()
|
|
}
|
|
|
|
tokenInfo, err := flow.WaitToken(ctx, flowInfo)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("wait for token: %w", err)
|
|
}
|
|
|
|
return &tokenInfo, nil
|
|
}
|