mirror of
https://github.com/netbirdio/netbird.git
synced 2024-11-22 16:13:31 +01:00
3def84b111
Support Generic OAuth 2.0 Device Authorization Grant as per RFC specification https://www.rfc-editor.org/rfc/rfc8628. The previous version supported only Auth0 as an IDP backend. This implementation enables the Interactive SSO Login feature for any IDP compatible with the specification, e.g., Keycloak.
267 lines
8.0 KiB
Go
267 lines
8.0 KiB
Go
package internal
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"github.com/netbirdio/netbird/client/ssh"
|
|
"github.com/netbirdio/netbird/iface"
|
|
mgm "github.com/netbirdio/netbird/management/client"
|
|
"github.com/netbirdio/netbird/util"
|
|
log "github.com/sirupsen/logrus"
|
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
"net/url"
|
|
"os"
|
|
)
|
|
|
|
var managementURLDefault *url.URL
|
|
|
|
func ManagementURLDefault() *url.URL {
|
|
return managementURLDefault
|
|
}
|
|
|
|
func init() {
|
|
managementURL, err := ParseURL("Management URL", "https://api.wiretrustee.com:443")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
managementURLDefault = managementURL
|
|
}
|
|
|
|
// Config Configuration type
|
|
type Config struct {
|
|
// Wireguard private key of local peer
|
|
PrivateKey string
|
|
PreSharedKey string
|
|
ManagementURL *url.URL
|
|
AdminURL *url.URL
|
|
WgIface string
|
|
IFaceBlackList []string
|
|
// SSHKey is a private SSH key in a PEM format
|
|
SSHKey string
|
|
}
|
|
|
|
// createNewConfig creates a new config generating a new Wireguard key and saving to file
|
|
func createNewConfig(managementURL, adminURL, configPath, preSharedKey string) (*Config, error) {
|
|
wgKey := generateKey()
|
|
pem, err := ssh.GeneratePrivateKey(ssh.ED25519)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
config := &Config{SSHKey: string(pem), PrivateKey: wgKey, WgIface: iface.WgInterfaceDefault, IFaceBlackList: []string{}}
|
|
if managementURL != "" {
|
|
URL, err := ParseURL("Management URL", managementURL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
config.ManagementURL = URL
|
|
} else {
|
|
config.ManagementURL = managementURLDefault
|
|
}
|
|
|
|
if preSharedKey != "" {
|
|
config.PreSharedKey = preSharedKey
|
|
}
|
|
|
|
if adminURL != "" {
|
|
newURL, err := ParseURL("Admin Panel URL", adminURL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
config.AdminURL = newURL
|
|
}
|
|
|
|
config.IFaceBlackList = []string{iface.WgInterfaceDefault, "tun0", "zt", "ZeroTier", "utun", "wg", "ts",
|
|
"Tailscale", "tailscale"}
|
|
|
|
err = util.WriteJson(configPath, config)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return config, nil
|
|
}
|
|
|
|
// ParseURL parses and validates management URL
|
|
func ParseURL(serviceName, managementURL string) (*url.URL, error) {
|
|
parsedMgmtURL, err := url.ParseRequestURI(managementURL)
|
|
if err != nil {
|
|
log.Errorf("failed parsing management URL %s: [%s]", managementURL, err.Error())
|
|
return nil, err
|
|
}
|
|
|
|
if parsedMgmtURL.Scheme != "https" && parsedMgmtURL.Scheme != "http" {
|
|
return nil, fmt.Errorf(
|
|
"invalid %s URL provided %s. Supported format [http|https]://[host]:[port]",
|
|
serviceName, managementURL)
|
|
}
|
|
|
|
return parsedMgmtURL, err
|
|
}
|
|
|
|
// ReadConfig reads existing config. In case provided managementURL is not empty overrides the read property
|
|
func ReadConfig(managementURL, adminURL, configPath string, preSharedKey *string) (*Config, error) {
|
|
config := &Config{}
|
|
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
|
return nil, status.Errorf(codes.NotFound, "config file doesn't exist")
|
|
}
|
|
|
|
if _, err := util.ReadJson(configPath, config); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
refresh := false
|
|
|
|
if managementURL != "" && config.ManagementURL.String() != managementURL {
|
|
log.Infof("new Management URL provided, updated to %s (old value %s)",
|
|
managementURL, config.ManagementURL)
|
|
newURL, err := ParseURL("Management URL", managementURL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
config.ManagementURL = newURL
|
|
refresh = true
|
|
}
|
|
|
|
if adminURL != "" && (config.AdminURL == nil || config.AdminURL.String() != adminURL) {
|
|
log.Infof("new Admin Panel URL provided, updated to %s (old value %s)",
|
|
adminURL, config.AdminURL)
|
|
newURL, err := ParseURL("Admin Panel URL", adminURL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
config.AdminURL = newURL
|
|
refresh = true
|
|
}
|
|
|
|
if preSharedKey != nil && config.PreSharedKey != *preSharedKey {
|
|
log.Infof("new pre-shared key provided, updated to %s (old value %s)",
|
|
*preSharedKey, config.PreSharedKey)
|
|
config.PreSharedKey = *preSharedKey
|
|
refresh = true
|
|
}
|
|
if config.SSHKey == "" {
|
|
pem, err := ssh.GeneratePrivateKey(ssh.ED25519)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
config.SSHKey = string(pem)
|
|
refresh = true
|
|
}
|
|
|
|
if refresh {
|
|
// since we have new management URL, we need to update config file
|
|
if err := util.WriteJson(configPath, config); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return config, nil
|
|
}
|
|
|
|
// GetConfig reads existing config or generates a new one
|
|
func GetConfig(managementURL, adminURL, configPath, preSharedKey string) (*Config, error) {
|
|
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
|
log.Infof("generating new config %s", configPath)
|
|
return createNewConfig(managementURL, adminURL, configPath, preSharedKey)
|
|
} else {
|
|
// don't overwrite pre-shared key if we receive asterisks from UI
|
|
pk := &preSharedKey
|
|
if preSharedKey == "**********" {
|
|
pk = nil
|
|
}
|
|
return ReadConfig(managementURL, adminURL, configPath, pk)
|
|
}
|
|
}
|
|
|
|
// generateKey generates a new Wireguard private key
|
|
func generateKey() string {
|
|
key, err := wgtypes.GeneratePrivateKey()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return key.String()
|
|
}
|
|
|
|
// DeviceAuthorizationFlow represents Device Authorization Flow information
|
|
type DeviceAuthorizationFlow struct {
|
|
Provider string
|
|
ProviderConfig ProviderConfig
|
|
}
|
|
|
|
// ProviderConfig has all attributes needed to initiate a device authorization flow
|
|
type ProviderConfig struct {
|
|
// ClientID An IDP application client id
|
|
ClientID string
|
|
// ClientSecret An IDP application client secret
|
|
ClientSecret string
|
|
// Domain An IDP API domain
|
|
// Deprecated. Use OIDCConfigEndpoint instead
|
|
Domain string
|
|
// Audience An Audience for to authorization validation
|
|
Audience string
|
|
// TokenEndpoint is the endpoint of an IDP manager where clients can obtain access token
|
|
TokenEndpoint string
|
|
// DeviceAuthEndpoint is the endpoint of an IDP manager where clients can obtain device authorization code
|
|
DeviceAuthEndpoint string
|
|
}
|
|
|
|
func GetDeviceAuthorizationFlowInfo(ctx context.Context, config *Config) (DeviceAuthorizationFlow, error) {
|
|
// validate our peer's Wireguard PRIVATE key
|
|
myPrivateKey, err := wgtypes.ParseKey(config.PrivateKey)
|
|
if err != nil {
|
|
log.Errorf("failed parsing Wireguard key %s: [%s]", config.PrivateKey, err.Error())
|
|
return DeviceAuthorizationFlow{}, err
|
|
}
|
|
|
|
var mgmTlsEnabled bool
|
|
if config.ManagementURL.Scheme == "https" {
|
|
mgmTlsEnabled = true
|
|
}
|
|
|
|
log.Debugf("connecting to Management Service %s", config.ManagementURL.String())
|
|
mgmClient, err := mgm.NewClient(ctx, config.ManagementURL.Host, myPrivateKey, mgmTlsEnabled)
|
|
if err != nil {
|
|
log.Errorf("failed connecting to Management Service %s %v", config.ManagementURL.String(), err)
|
|
return DeviceAuthorizationFlow{}, err
|
|
}
|
|
log.Debugf("connected to management Service %s", config.ManagementURL.String())
|
|
|
|
serverKey, err := mgmClient.GetServerPublicKey()
|
|
if err != nil {
|
|
log.Errorf("failed while getting Management Service public key: %v", err)
|
|
return DeviceAuthorizationFlow{}, err
|
|
}
|
|
|
|
protoDeviceAuthorizationFlow, err := mgmClient.GetDeviceAuthorizationFlow(*serverKey)
|
|
if err != nil {
|
|
if s, ok := status.FromError(err); ok && s.Code() == codes.NotFound {
|
|
log.Warnf("server couldn't find device flow, contact admin: %v", err)
|
|
return DeviceAuthorizationFlow{}, err
|
|
} else {
|
|
log.Errorf("failed to retrieve device flow: %v", err)
|
|
return DeviceAuthorizationFlow{}, err
|
|
}
|
|
}
|
|
|
|
err = mgmClient.Close()
|
|
if err != nil {
|
|
log.Errorf("failed closing Management Service client: %v", err)
|
|
return DeviceAuthorizationFlow{}, err
|
|
}
|
|
|
|
return DeviceAuthorizationFlow{
|
|
Provider: protoDeviceAuthorizationFlow.Provider.String(),
|
|
|
|
ProviderConfig: ProviderConfig{
|
|
Audience: protoDeviceAuthorizationFlow.GetProviderConfig().GetAudience(),
|
|
ClientID: protoDeviceAuthorizationFlow.GetProviderConfig().GetClientID(),
|
|
ClientSecret: protoDeviceAuthorizationFlow.GetProviderConfig().GetClientSecret(),
|
|
Domain: protoDeviceAuthorizationFlow.GetProviderConfig().Domain,
|
|
TokenEndpoint: protoDeviceAuthorizationFlow.GetProviderConfig().GetTokenEndpoint(),
|
|
DeviceAuthEndpoint: protoDeviceAuthorizationFlow.GetProviderConfig().GetDeviceAuthEndpoint(),
|
|
},
|
|
}, nil
|
|
}
|