mirror of
https://github.com/netbirdio/netbird.git
synced 2025-04-01 03:36:16 +02:00
Refactor the auth code into async calls for mobile framework --------- Co-authored-by: Maycon Santos <mlsmaycon@gmail.com>
235 lines
6.5 KiB
Go
235 lines
6.5 KiB
Go
package android
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/cenkalti/backoff/v4"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
"google.golang.org/grpc/codes"
|
|
gstatus "google.golang.org/grpc/status"
|
|
|
|
"github.com/netbirdio/netbird/client/cmd"
|
|
"github.com/netbirdio/netbird/client/system"
|
|
|
|
"github.com/netbirdio/netbird/client/internal"
|
|
)
|
|
|
|
// 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(string)
|
|
}
|
|
|
|
// Auth can register or login new client
|
|
type Auth struct {
|
|
ctx context.Context
|
|
config *internal.Config
|
|
cfgPath string
|
|
}
|
|
|
|
// NewAuth instantiate Auth struct and validate the management URL
|
|
func NewAuth(cfgPath string, mgmURL string) (*Auth, error) {
|
|
inputCfg := internal.ConfigInput{
|
|
ManagementURL: mgmURL,
|
|
}
|
|
|
|
cfg, err := internal.CreateInMemoryConfig(inputCfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &Auth{
|
|
ctx: context.Background(),
|
|
config: cfg,
|
|
cfgPath: cfgPath,
|
|
}, nil
|
|
}
|
|
|
|
// NewAuthWithConfig instantiate Auth based on existing config
|
|
func NewAuthWithConfig(ctx context.Context, config *internal.Config) *Auth {
|
|
return &Auth{
|
|
ctx: ctx,
|
|
config: config,
|
|
}
|
|
}
|
|
|
|
// SaveConfigIfSSOSupported test the connectivity with the management server by retrieving the server device flow info.
|
|
// If it returns a flow info than save the configuration and return true. If it gets a codes.NotFound, it means that SSO
|
|
// is not supported and returns false without saving the configuration. For other errors return false.
|
|
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) {
|
|
supportsSSO := true
|
|
err := a.withBackOff(a.ctx, func() (err error) {
|
|
_, err = internal.GetDeviceAuthorizationFlowInfo(a.ctx, a.config.PrivateKey, a.config.ManagementURL)
|
|
if s, ok := gstatus.FromError(err); ok && s.Code() == codes.NotFound {
|
|
supportsSSO = false
|
|
err = nil
|
|
}
|
|
return err
|
|
})
|
|
|
|
if !supportsSSO {
|
|
return false, nil
|
|
}
|
|
|
|
if err != nil {
|
|
return false, fmt.Errorf("backoff cycle failed: %v", err)
|
|
}
|
|
|
|
err = internal.WriteOutConfig(a.cfgPath, a.config)
|
|
return true, err
|
|
}
|
|
|
|
// LoginWithSetupKeyAndSaveConfig test the connectivity with the management server with the setup key.
|
|
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 {
|
|
//nolint
|
|
ctxWithValues := context.WithValue(a.ctx, system.DeviceNameCtxKey, deviceName)
|
|
|
|
err := a.withBackOff(a.ctx, func() error {
|
|
backoffErr := internal.Login(ctxWithValues, a.config, setupKey, "")
|
|
if s, ok := gstatus.FromError(backoffErr); ok && (s.Code() == codes.PermissionDenied) {
|
|
// we got an answer from management, exit backoff earlier
|
|
return backoff.Permanent(backoffErr)
|
|
}
|
|
return backoffErr
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("backoff cycle failed: %v", err)
|
|
}
|
|
|
|
return internal.WriteOutConfig(a.cfgPath, a.config)
|
|
}
|
|
|
|
// Login try register the client on the server
|
|
func (a *Auth) Login(resultListener ErrListener, urlOpener URLOpener) {
|
|
go func() {
|
|
err := a.login(urlOpener)
|
|
if err != nil {
|
|
resultListener.OnError(err)
|
|
} else {
|
|
resultListener.OnSuccess()
|
|
}
|
|
}()
|
|
}
|
|
|
|
func (a *Auth) login(urlOpener URLOpener) error {
|
|
var needsLogin bool
|
|
|
|
// check if we need to generate JWT token
|
|
err := a.withBackOff(a.ctx, func() (err error) {
|
|
needsLogin, err = internal.IsLoginRequired(a.ctx, a.config.PrivateKey, a.config.ManagementURL, a.config.SSHKey)
|
|
return
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("backoff cycle failed: %v", err)
|
|
}
|
|
|
|
jwtToken := ""
|
|
if needsLogin {
|
|
tokenInfo, err := a.foregroundGetTokenInfo(urlOpener)
|
|
if err != nil {
|
|
return fmt.Errorf("interactive sso login failed: %v", err)
|
|
}
|
|
jwtToken = tokenInfo.AccessToken
|
|
}
|
|
|
|
err = a.withBackOff(a.ctx, func() error {
|
|
err := internal.Login(a.ctx, a.config, "", jwtToken)
|
|
if s, ok := gstatus.FromError(err); ok && (s.Code() == codes.InvalidArgument || s.Code() == codes.PermissionDenied) {
|
|
return nil
|
|
}
|
|
return err
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("backoff cycle failed: %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *Auth) foregroundGetTokenInfo(urlOpener URLOpener) (*internal.TokenInfo, error) {
|
|
providerConfig, err := internal.GetDeviceAuthorizationFlowInfo(a.ctx, a.config.PrivateKey, a.config.ManagementURL)
|
|
if err != nil {
|
|
s, ok := gstatus.FromError(err)
|
|
if ok && s.Code() == codes.NotFound {
|
|
return nil, fmt.Errorf("no SSO provider returned from management. " +
|
|
"If you are using hosting Netbird see documentation at " +
|
|
"https://github.com/netbirdio/netbird/tree/main/management for details")
|
|
} else if ok && s.Code() == codes.Unimplemented {
|
|
return nil, fmt.Errorf("the management server, %s, does not support SSO providers, "+
|
|
"please update your servver or use Setup Keys to login", a.config.ManagementURL)
|
|
} else {
|
|
return nil, fmt.Errorf("getting device authorization flow info failed with error: %v", err)
|
|
}
|
|
}
|
|
|
|
hostedClient := internal.NewHostedDeviceFlow(
|
|
providerConfig.ProviderConfig.Audience,
|
|
providerConfig.ProviderConfig.ClientID,
|
|
providerConfig.ProviderConfig.TokenEndpoint,
|
|
providerConfig.ProviderConfig.DeviceAuthEndpoint,
|
|
)
|
|
|
|
flowInfo, err := hostedClient.RequestDeviceCode(context.TODO())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("getting a request device code failed: %v", err)
|
|
}
|
|
|
|
go urlOpener.Open(flowInfo.VerificationURIComplete)
|
|
|
|
waitTimeout := time.Duration(flowInfo.ExpiresIn)
|
|
waitCTX, cancel := context.WithTimeout(a.ctx, waitTimeout*time.Second)
|
|
defer cancel()
|
|
tokenInfo, err := hostedClient.WaitToken(waitCTX, flowInfo)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("waiting for browser login failed: %v", err)
|
|
}
|
|
|
|
return &tokenInfo, nil
|
|
}
|
|
|
|
func (a *Auth) withBackOff(ctx context.Context, bf func() error) error {
|
|
return backoff.RetryNotify(
|
|
bf,
|
|
backoff.WithContext(cmd.CLIBackOffSettings, ctx),
|
|
func(err error, duration time.Duration) {
|
|
log.Warnf("retrying Login to the Management service in %v due to error %v", duration, err)
|
|
})
|
|
}
|