mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-22 10:16:38 +00:00
Enhance the user experience by enabling authentication to Netbird using Single Sign-On (SSO) with any Identity Provider (IDP) provider. Current client offers this capability through the Device Authorization Flow, however, is not widely supported by many IDPs, and even some that do support it do not provide a complete verification URL. To address these challenges, this pull request enable Authorization Code Flow with Proof Key for Code Exchange (PKCE) for client logins, which is a more widely adopted and secure approach to facilitate SSO with various IDP providers.
208 lines
5.4 KiB
Go
208 lines
5.4 KiB
Go
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"github.com/netbirdio/netbird/client/internal/auth"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/skratchdot/open-golang/open"
|
|
"google.golang.org/grpc/codes"
|
|
gstatus "google.golang.org/grpc/status"
|
|
|
|
"github.com/netbirdio/netbird/util"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"github.com/netbirdio/netbird/client/internal"
|
|
"github.com/netbirdio/netbird/client/proto"
|
|
"github.com/netbirdio/netbird/client/system"
|
|
)
|
|
|
|
var loginCmd = &cobra.Command{
|
|
Use: "login",
|
|
Short: "login to the Netbird Management Service (first run)",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
SetFlagsFromEnvVars(rootCmd)
|
|
|
|
cmd.SetOut(cmd.OutOrStdout())
|
|
|
|
err := util.InitLog(logLevel, "console")
|
|
if err != nil {
|
|
return fmt.Errorf("failed initializing log %v", err)
|
|
}
|
|
|
|
ctx := internal.CtxInitState(context.Background())
|
|
|
|
if hostName != "" {
|
|
// nolint
|
|
ctx = context.WithValue(ctx, system.DeviceNameCtxKey, hostName)
|
|
}
|
|
|
|
// workaround to run without service
|
|
if logFile == "console" {
|
|
err = handleRebrand(cmd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ic := internal.ConfigInput{
|
|
ManagementURL: managementURL,
|
|
AdminURL: adminURL,
|
|
ConfigPath: configPath,
|
|
}
|
|
if preSharedKey != "" {
|
|
ic.PreSharedKey = &preSharedKey
|
|
}
|
|
|
|
config, err := internal.UpdateOrCreateConfig(ic)
|
|
if err != nil {
|
|
return fmt.Errorf("get config file: %v", err)
|
|
}
|
|
|
|
config, _ = internal.UpdateOldManagementPort(ctx, config, configPath)
|
|
|
|
err = foregroundLogin(ctx, cmd, config, setupKey)
|
|
if err != nil {
|
|
return fmt.Errorf("foreground login failed: %v", err)
|
|
}
|
|
cmd.Println("Logging successfully")
|
|
return nil
|
|
}
|
|
|
|
conn, err := DialClientGRPCServer(ctx, daemonAddr)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to connect to daemon error: %v\n"+
|
|
"If the daemon is not running please run: "+
|
|
"\nnetbird service install \nnetbird service start\n", err)
|
|
}
|
|
defer conn.Close()
|
|
|
|
client := proto.NewDaemonServiceClient(conn)
|
|
|
|
loginRequest := proto.LoginRequest{
|
|
SetupKey: setupKey,
|
|
PreSharedKey: preSharedKey,
|
|
ManagementUrl: managementURL,
|
|
}
|
|
|
|
var loginErr error
|
|
|
|
var loginResp *proto.LoginResponse
|
|
|
|
err = WithBackOff(func() error {
|
|
var backOffErr error
|
|
loginResp, backOffErr = client.Login(ctx, &loginRequest)
|
|
if s, ok := gstatus.FromError(backOffErr); ok && (s.Code() == codes.InvalidArgument ||
|
|
s.Code() == codes.PermissionDenied ||
|
|
s.Code() == codes.NotFound ||
|
|
s.Code() == codes.Unimplemented) {
|
|
loginErr = backOffErr
|
|
return nil
|
|
}
|
|
return backOffErr
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("login backoff cycle failed: %v", err)
|
|
}
|
|
|
|
if loginErr != nil {
|
|
return fmt.Errorf("login failed: %v", loginErr)
|
|
}
|
|
|
|
if loginResp.NeedsSSOLogin {
|
|
openURL(cmd, loginResp.VerificationURIComplete, loginResp.UserCode)
|
|
|
|
_, err = client.WaitSSOLogin(ctx, &proto.WaitSSOLoginRequest{UserCode: loginResp.UserCode})
|
|
if err != nil {
|
|
return fmt.Errorf("waiting sso login failed with: %v", err)
|
|
}
|
|
}
|
|
|
|
cmd.Println("Logging successfully")
|
|
|
|
return nil
|
|
},
|
|
}
|
|
|
|
func foregroundLogin(ctx context.Context, cmd *cobra.Command, config *internal.Config, setupKey string) error {
|
|
needsLogin := false
|
|
|
|
err := WithBackOff(func() error {
|
|
err := internal.Login(ctx, config, "", "")
|
|
if s, ok := gstatus.FromError(err); ok && (s.Code() == codes.InvalidArgument || s.Code() == codes.PermissionDenied) {
|
|
needsLogin = true
|
|
return nil
|
|
}
|
|
return err
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("backoff cycle failed: %v", err)
|
|
}
|
|
|
|
jwtToken := ""
|
|
if setupKey == "" && needsLogin {
|
|
tokenInfo, err := foregroundGetTokenInfo(ctx, cmd, config)
|
|
if err != nil {
|
|
return fmt.Errorf("interactive sso login failed: %v", err)
|
|
}
|
|
jwtToken = tokenInfo.GetTokenToUse()
|
|
}
|
|
|
|
err = WithBackOff(func() error {
|
|
err := internal.Login(ctx, config, setupKey, 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 foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *internal.Config) (*auth.TokenInfo, error) {
|
|
oAuthFlow, err := auth.NewOAuthFlow(ctx, config)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
flowInfo, err := oAuthFlow.RequestAuthInfo(context.TODO())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("getting a request OAuth flow info failed: %v", err)
|
|
}
|
|
|
|
openURL(cmd, flowInfo.VerificationURIComplete, flowInfo.UserCode)
|
|
|
|
waitTimeout := time.Duration(flowInfo.ExpiresIn)
|
|
waitCTX, c := context.WithTimeout(context.TODO(), waitTimeout*time.Second)
|
|
defer c()
|
|
|
|
tokenInfo, err := oAuthFlow.WaitToken(waitCTX, flowInfo)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("waiting for browser login failed: %v", err)
|
|
}
|
|
|
|
return &tokenInfo, nil
|
|
}
|
|
|
|
func openURL(cmd *cobra.Command, verificationURIComplete, userCode string) {
|
|
var codeMsg string
|
|
if userCode != "" {
|
|
if !strings.Contains(verificationURIComplete, userCode) {
|
|
codeMsg = fmt.Sprintf("and enter the code %s to authenticate.", userCode)
|
|
}
|
|
}
|
|
|
|
err := open.Run(verificationURIComplete)
|
|
cmd.Printf("Please do the SSO login in your browser. \n" +
|
|
"If your browser didn't open automatically, use this URL to log in:\n\n" +
|
|
" " + verificationURIComplete + " " + codeMsg + " \n\n")
|
|
if err != nil {
|
|
cmd.Printf("Alternatively, you may want to use a setup key, see:\n\n https://www.netbird.io/docs/overview/setup-keys\n")
|
|
}
|
|
}
|