Skip to content
Closed
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
21 changes: 9 additions & 12 deletions client/cmd/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"fmt"
"os"
"os/user"
"runtime"
"strings"

log "github.com/sirupsen/logrus"
Expand Down Expand Up @@ -121,7 +120,7 @@ func doDaemonLogin(ctx context.Context, cmd *cobra.Command, providedSetupKey str
loginRequest := proto.LoginRequest{
SetupKey: providedSetupKey,
ManagementUrl: managementURL,
IsUnixDesktopClient: isUnixRunningDesktop(),
IsUnixDesktopClient: util.HasGraphicalSession(),
Hostname: hostName,
DnsLabels: dnsLabelsReq,
ProfileName: &handle,
Expand Down Expand Up @@ -189,7 +188,8 @@ func doExtendSession(ctx context.Context, cmd *cobra.Command) error {

client := proto.NewDaemonServiceClient(conn)

req := &proto.RequestExtendAuthSessionRequest{}
// the CLI runs in the user's session, the daemon does not: tell it what we can see
req := &proto.RequestExtendAuthSessionRequest{HasGraphicalSession: util.HasGraphicalSession()}
// Pre-fill the IdP login hint from the active profile so the user
// doesn't have to retype their email. Best-effort: we still proceed
// without a hint if the lookup fails.
Expand Down Expand Up @@ -408,8 +408,13 @@ func foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *pro
hint = profileState.Email
}

oAuthFlow, err := auth.NewOAuthFlow(ctx, config, isUnixRunningDesktop(), false, hint)
oAuthFlow, err := auth.NewOAuthFlow(ctx, config, util.HasGraphicalSession(), false, hint)
if err != nil {
// enrolling a device is the one flow a setup key can replace
if auth.IsSSOUnavailable(err) {
return nil, fmt.Errorf("%w. Set this device up with a setup key instead: "+
"https://docs.netbird.io/how-to/register-machines-using-setup-keys", err)
}
return nil, err
}

Expand Down Expand Up @@ -458,14 +463,6 @@ func openURL(cmd *cobra.Command, verificationURIComplete, userCode string, noBro
}
}

// isUnixRunningDesktop checks if a Linux OS is running desktop environment
func isUnixRunningDesktop() bool {
if runtime.GOOS != "linux" && runtime.GOOS != "freebsd" {
return false
}
return os.Getenv("DESKTOP_SESSION") != "" || os.Getenv("XDG_CURRENT_DESKTOP") != ""
}

func setEnvAndFlags(cmd *cobra.Command) error {
SetFlagsFromEnvVars(rootCmd)

Expand Down
4 changes: 2 additions & 2 deletions client/cmd/up.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ import (
"github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/internal/profilemanager"
"github.com/netbirdio/netbird/client/proto"
nbnet "github.com/netbirdio/netbird/client/net"
"github.com/netbirdio/netbird/client/proto"
"github.com/netbirdio/netbird/client/server"
"github.com/netbirdio/netbird/client/system"
"github.com/netbirdio/netbird/shared/management/domain"
Expand Down Expand Up @@ -626,7 +626,7 @@ func setupLoginRequest(providedSetupKey string, customDNSAddressConverted []byte
NatExternalIPs: natExternalIPs,
CleanNATExternalIPs: natExternalIPs != nil && len(natExternalIPs) == 0,
CustomDNSAddress: customDNSAddressConverted,
IsUnixDesktopClient: isUnixRunningDesktop(),
IsUnixDesktopClient: util.HasGraphicalSession(),
Hostname: hostName,
ExtraIFaceBlacklist: extraIFaceBlackList,
DnsLabels: dnsLabels,
Expand Down
29 changes: 13 additions & 16 deletions client/internal/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package auth

import (
"context"
"errors"
"net/url"
"strings"
"sync"
Expand Down Expand Up @@ -140,25 +141,21 @@ func (a *Auth) IsSSOSupported(ctx context.Context) (bool, error) {
// This avoids creating a new connection to the management server
func (a *Auth) GetOAuthFlow(ctx context.Context, forceDeviceAuth bool) (OAuthFlow, error) {
var flow OAuthFlow
var err error

err = a.withRetry(ctx, func(client *mgm.GrpcClient) error {
if forceDeviceAuth {
flow, err = a.getDeviceFlow(client)
return err
}
// the connection is owned by a and outlives this call, so a later fallback reuses it
newAuth := func(context.Context) (*Auth, func(), error) {
return a, func() {}, nil
}

// Try PKCE flow first
flow, err = a.getPKCEFlow(client)
if err != nil {
// If PKCE not supported, try Device flow
if s, ok := status.FromError(err); ok && (s.Code() == codes.NotFound || s.Code() == codes.Unimplemented) {
flow, err = a.getDeviceFlow(client)
return err
}
return err
err := a.withRetry(ctx, func(client *mgm.GrpcClient) error {
var err error
flow, err = oauthFlowWithFallback(a, client, flowOrder(forceDeviceAuth), "", newAuth)

var ssoUnavailable *ssoUnavailableError
if errors.As(err, &ssoUnavailable) {
return backoff.Permanent(err)
}
return nil
return err
})

return flow, err
Expand Down
47 changes: 44 additions & 3 deletions client/internal/auth/device_flow.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,17 @@ type DeviceAuthProviderConfig struct {
LoginHint string
}

// validateDeviceAuthConfig validates device authorization provider configuration
// validateDeviceAuthConfig validates device authorization provider configuration. A missing
// value means management does not have this flow configured, so the error wraps
// errFlowNotConfigured and the caller can fall back to the other flow.
func validateDeviceAuthConfig(config *DeviceAuthProviderConfig) error {
if err := checkDeviceAuthConfig(config); err != nil {
return fmt.Errorf("%w: %w", errFlowNotConfigured, err)
}
return nil
}

func checkDeviceAuthConfig(config *DeviceAuthProviderConfig) error {
errorMsgFormat := "invalid provider configuration received from management: %s value is empty. Contact your NetBird administrator"

if config.Audience == "" {
Expand Down Expand Up @@ -161,8 +170,12 @@ func (d *DeviceAuthorizationFlow) RequestAuthInfo(ctx context.Context) (AuthFlow
return AuthFlowInfo{}, fmt.Errorf("reading body failed with error: %v", err)
}

if res.StatusCode != 200 {
return AuthFlowInfo{}, fmt.Errorf("request device code returned status %d error: %s", res.StatusCode, string(body))
if res.StatusCode != http.StatusOK {
reqErr := fmt.Errorf("request device code returned status %d error: %s", res.StatusCode, string(body))
if deviceGrantUnsupported(res.StatusCode, body) {
return AuthFlowInfo{}, fmt.Errorf("%w: %w", errFlowNotConfigured, reqErr)
}
return AuthFlowInfo{}, reqErr
}

deviceCode := AuthFlowInfo{}
Expand All @@ -186,6 +199,34 @@ func (d *DeviceAuthorizationFlow) RequestAuthInfo(ctx context.Context) (AuthFlow
return deviceCode, err
}

// deviceGrantUnsupported reports whether the IdP's answer to a device code request means it does
// not serve the device authorization grant at all, rather than a transient or request-specific
// failure. An IdP that does not route the endpoint answers 404/405/501; one that knows the
// endpoint but has the grant disabled for this client answers with an OAuth 2.0 error code.
func deviceGrantUnsupported(statusCode int, body []byte) bool {
switch statusCode {
case http.StatusNotFound, http.StatusMethodNotAllowed, http.StatusNotImplemented:
return true
case http.StatusBadRequest, http.StatusUnauthorized, http.StatusForbidden:
default:
return false
}

var oauthErr struct {
Error string `json:"error"`
}
if err := json.Unmarshal(body, &oauthErr); err != nil {
return false
}

switch oauthErr.Error {
case "unsupported_grant_type", "unauthorized_client", "invalid_client":
return true
default:
return false
}
}

func appendLoginHint(uri, loginHint string) string {
if uri == "" || loginHint == "" {
return uri
Expand Down
Loading
Loading