Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion client/cmd/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ var extendSessionFlag bool

func init() {
loginCmd.PersistentFlags().BoolVar(&noBrowser, noBrowserFlag, false, noBrowserDesc)
loginCmd.PersistentFlags().BoolVar(&useDeviceAuth, useDeviceAuthFlag, false, useDeviceAuthDesc)
loginCmd.PersistentFlags().BoolVar(&showQR, showQRFlag, false, showQRDesc)
loginCmd.PersistentFlags().StringVar(&profileName, profileNameFlag, "", profileNameDesc)
loginCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "(DEPRECATED) Netbird config file location")
Expand Down Expand Up @@ -137,6 +138,10 @@ func doDaemonLogin(ctx context.Context, cmd *cobra.Command, providedSetupKey str
loginRequest.OptionalPreSharedKey = &preSharedKey
}

if cmd.Flags().Changed(useDeviceAuthFlag) {
loginRequest.UseDeviceAuth = &useDeviceAuth
}

var loginErr error

var loginResp *proto.LoginResponse
Expand Down Expand Up @@ -406,7 +411,7 @@ func foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *pro
hint = profileState.Email
}

oAuthFlow, err := auth.NewOAuthFlow(ctx, config, util.HasGraphicalSession(), false, hint)
oAuthFlow, err := auth.NewOAuthFlow(ctx, config, util.HasGraphicalSession(), useDeviceAuth, hint)
if err != nil {
return nil, err
}
Expand Down
9 changes: 9 additions & 0 deletions client/cmd/up.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ const (
noBrowserFlag = "no-browser"
noBrowserDesc = "do not open the browser for SSO login"

useDeviceAuthFlag = "use-device-auth"
useDeviceAuthDesc = "force the OAuth 2.0 Device Authorization Grant instead of the PKCE/browser flow"

showQRFlag = "qr"
showQRDesc = "show QR code for the SSO login URL (useful for headless machines without browser access)"

Expand All @@ -55,6 +58,7 @@ var (
dnsLabels []string
dnsLabelsValidated domain.List
noBrowser bool
useDeviceAuth bool
showQR bool
profileName string
configPath string
Expand Down Expand Up @@ -88,6 +92,7 @@ func init() {
)

upCmd.PersistentFlags().BoolVar(&noBrowser, noBrowserFlag, false, noBrowserDesc)
upCmd.PersistentFlags().BoolVar(&useDeviceAuth, useDeviceAuthFlag, false, useDeviceAuthDesc)
upCmd.PersistentFlags().BoolVar(&showQR, showQRFlag, false, showQRDesc)
upCmd.PersistentFlags().StringVar(&profileName, profileNameFlag, "", profileNameDesc)
upCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "(DEPRECATED) NetBird config file location. ")
Expand Down Expand Up @@ -731,6 +736,10 @@ func setupLoginRequest(providedSetupKey string, customDNSAddressConverted []byte
loginRequest.OptionalPreSharedKey = &preSharedKey
}

if cmd.Flags().Changed(useDeviceAuthFlag) {
loginRequest.UseDeviceAuth = &useDeviceAuth
}

if cmd.Flag(enableRosenpassFlag).Changed {
loginRequest.RosenpassEnabled = &rosenpassEnabled
}
Expand Down
20 changes: 20 additions & 0 deletions client/internal/auth/oauth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package auth

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestShouldUseDeviceFlow_ForcedAlwaysTrue(t *testing.T) {
// The --use-device-auth flag relies on force=true overriding the
// automatic PKCE-vs-device selection, even on a desktop client where
// PKCE would otherwise be chosen.
assert.True(t, shouldUseDeviceFlow(true, true), "force should select device flow on a desktop client")
assert.True(t, shouldUseDeviceFlow(true, false), "force should select device flow on a non-desktop client")
}

func TestShouldUseDeviceFlow_NotForcedRespectsDesktop(t *testing.T) {
// Without force, a desktop client must not be pushed to device flow.
assert.False(t, shouldUseDeviceFlow(false, true), "desktop client without force should not use device flow")
}
24 changes: 18 additions & 6 deletions client/proto/daemon.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions client/proto/daemon.proto
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,9 @@ message LoginRequest {
// remoteJobsAllowed opts the peer into management-requested remote jobs
// (e.g. debug bundles). Absent leaves the stored value unchanged.
optional bool remoteJobsAllowed = 43;
// useDeviceAuth forces the OAuth 2.0 Device Authorization Grant instead of
// the PKCE/browser flow.
optional bool useDeviceAuth = 44;
}

message LoginResponse {
Expand Down
2 changes: 1 addition & 1 deletion client/proto/daemon_grpc.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 8 additions & 3 deletions client/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -742,13 +742,13 @@ func (s *Server) beginSSOLogin(ctx context.Context, config *profilemanager.Confi
if msg.Hint != nil {
hint = *msg.Hint
}
oAuthFlow, err := auth.NewOAuthFlow(ctx, config, msg.IsUnixDesktopClient, false, hint)
oAuthFlow, err := auth.NewOAuthFlow(ctx, config, msg.IsUnixDesktopClient, msg.GetUseDeviceAuth(), hint)
if err != nil {
state.Set(internal.StatusLoginFailed)
return nil, err
}

if resp := s.pendingOAuthFlowResponse(ctx, oAuthFlow); resp != nil {
if resp := s.pendingOAuthFlowResponse(ctx, oAuthFlow, msg.GetUseDeviceAuth()); resp != nil {
state.Set(internal.StatusNeedsLogin)
return resp, nil
}
Expand Down Expand Up @@ -780,11 +780,16 @@ func (s *Server) beginSSOLogin(ctx context.Context, config *profilemanager.Confi
// the browser leg, so a second login joins the pending flow instead of opening
// a competing one. A flow too close to expiry has its waiter cancelled and nil
// returned, leaving the caller to start a fresh flow.
func (s *Server) pendingOAuthFlowResponse(ctx context.Context, oAuthFlow auth.OAuthFlow) *proto.LoginResponse {
func (s *Server) pendingOAuthFlowResponse(ctx context.Context, oAuthFlow auth.OAuthFlow, useDeviceAuth bool) *proto.LoginResponse {
if s.oauthAuthFlow.flow == nil || s.oauthAuthFlow.flow.GetClientID(ctx) != oAuthFlow.GetClientID(ctx) {
return nil
}

_, cachedIsDevice := s.oauthAuthFlow.flow.(*auth.DeviceAuthorizationFlow)
if cachedIsDevice != useDeviceAuth {
return nil
}

if s.oauthAuthFlow.expiresAt.After(time.Now().Add(90 * time.Second)) {
log.Debugf("using previous oauth flow info")
return &proto.LoginResponse{
Expand Down