add support for auth plugins

This commit is contained in:
Nicola Murino 2021-08-08 17:09:48 +02:00
parent ced2e16f41
commit a20373b613
No known key found for this signature in database
GPG key ID: 2F1FB59433D5A8CB
17 changed files with 1885 additions and 91 deletions

View file

@ -16,7 +16,7 @@ import (
"github.com/drakkan/sftpgo/v2/util"
)
// HostEvent is the enumerable for the support host event
// HostEvent is the enumerable for the support host events
type HostEvent int
// Supported host events

View file

@ -356,23 +356,6 @@ func (d *BackupData) HasFolder(name string) bool {
return false
}
type keyboardAuthHookRequest struct {
RequestID string `json:"request_id"`
Username string `json:"username,omitempty"`
IP string `json:"ip,omitempty"`
Password string `json:"password,omitempty"`
Answers []string `json:"answers,omitempty"`
Questions []string `json:"questions,omitempty"`
}
type keyboardAuthHookResponse struct {
Instruction string `json:"instruction"`
Questions []string `json:"questions"`
Echos []bool `json:"echos"`
AuthResult int `json:"auth_result"`
CheckPwd int `json:"check_password"`
}
type checkPasswordRequest struct {
Username string `json:"username"`
IP string `json:"ip"`
@ -680,17 +663,15 @@ func CheckCompositeCredentials(username, password, ip, loginMethod, protocol str
return user, loginMethod, fmt.Errorf("certificate login method is not allowed for user %#v", user.Username)
}
if loginMethod == LoginMethodTLSCertificateAndPwd {
if config.ExternalAuthHook != "" && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&1 != 0) {
if plugin.Handler.HasAuthScope(plugin.AuthScopePassword) {
user, err = doPluginAuth(username, password, nil, ip, protocol, nil, plugin.AuthScopePassword)
} else if config.ExternalAuthHook != "" && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&1 != 0) {
user, err = doExternalAuth(username, password, nil, "", ip, protocol, nil)
if err != nil {
return user, loginMethod, err
}
}
if config.PreLoginHook != "" {
} else if config.PreLoginHook != "" {
user, err = executePreLoginHook(username, LoginMethodPassword, ip, protocol)
if err != nil {
return user, loginMethod, err
}
}
if err != nil {
return user, loginMethod, err
}
user, err = checkUserAndPass(&user, password, ip, protocol)
}
@ -699,6 +680,9 @@ func CheckCompositeCredentials(username, password, ip, loginMethod, protocol str
// CheckUserBeforeTLSAuth checks if a user exits before trying mutual TLS
func CheckUserBeforeTLSAuth(username, ip, protocol string, tlsCert *x509.Certificate) (User, error) {
if plugin.Handler.HasAuthScope(plugin.AuthScopeTLSCertificate) {
return doPluginAuth(username, "", nil, ip, protocol, tlsCert, plugin.AuthScopeTLSCertificate)
}
if config.ExternalAuthHook != "" && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&8 != 0) {
return doExternalAuth(username, "", nil, "", ip, protocol, tlsCert)
}
@ -711,6 +695,13 @@ func CheckUserBeforeTLSAuth(username, ip, protocol string, tlsCert *x509.Certifi
// CheckUserAndTLSCert returns the SFTPGo user with the given username and check if the
// given TLS certificate allow authentication without password
func CheckUserAndTLSCert(username, ip, protocol string, tlsCert *x509.Certificate) (User, error) {
if plugin.Handler.HasAuthScope(plugin.AuthScopeTLSCertificate) {
user, err := doPluginAuth(username, "", nil, ip, protocol, tlsCert, plugin.AuthScopeTLSCertificate)
if err != nil {
return user, err
}
return checkUserAndTLSCertificate(&user, protocol, tlsCert)
}
if config.ExternalAuthHook != "" && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&8 != 0) {
user, err := doExternalAuth(username, "", nil, "", ip, protocol, tlsCert)
if err != nil {
@ -730,6 +721,13 @@ func CheckUserAndTLSCert(username, ip, protocol string, tlsCert *x509.Certificat
// CheckUserAndPass retrieves the SFTPGo user with the given username and password if a match is found or an error
func CheckUserAndPass(username, password, ip, protocol string) (User, error) {
if plugin.Handler.HasAuthScope(plugin.AuthScopePassword) {
user, err := doPluginAuth(username, password, nil, ip, protocol, nil, plugin.AuthScopePassword)
if err != nil {
return user, err
}
return checkUserAndPass(&user, password, ip, protocol)
}
if config.ExternalAuthHook != "" && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&1 != 0) {
user, err := doExternalAuth(username, password, nil, "", ip, protocol, nil)
if err != nil {
@ -749,6 +747,13 @@ func CheckUserAndPass(username, password, ip, protocol string) (User, error) {
// CheckUserAndPubKey retrieves the SFTP user with the given username and public key if a match is found or an error
func CheckUserAndPubKey(username string, pubKey []byte, ip, protocol string) (User, string, error) {
if plugin.Handler.HasAuthScope(plugin.AuthScopePublicKey) {
user, err := doPluginAuth(username, "", pubKey, ip, protocol, nil, plugin.AuthScopePublicKey)
if err != nil {
return user, "", err
}
return checkUserAndPubKey(&user, pubKey)
}
if config.ExternalAuthHook != "" && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&2 != 0) {
user, err := doExternalAuth(username, "", pubKey, "", ip, protocol, nil)
if err != nil {
@ -771,7 +776,9 @@ func CheckUserAndPubKey(username string, pubKey []byte, ip, protocol string) (Us
func CheckKeyboardInteractiveAuth(username, authHook string, client ssh.KeyboardInteractiveChallenge, ip, protocol string) (User, error) {
var user User
var err error
if config.ExternalAuthHook != "" && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&4 != 0) {
if plugin.Handler.HasAuthScope(plugin.AuthScopeKeyboardInteractive) {
user, err = doPluginAuth(username, "", nil, ip, protocol, nil, plugin.AuthScopeKeyboardInteractive)
} else if config.ExternalAuthHook != "" && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&4 != 0) {
user, err = doExternalAuth(username, "", nil, "1", ip, protocol, nil)
} else if config.PreLoginHook != "" {
user, err = executePreLoginHook(username, SSHLoginMethodKeyboardInteractive, ip, protocol)
@ -1824,51 +1831,79 @@ func terminateInteractiveAuthProgram(cmd *exec.Cmd, isFinished bool) {
}
}
func validateKeyboardAuthResponse(response keyboardAuthHookResponse) error {
if len(response.Questions) == 0 {
err := errors.New("interactive auth error: hook response does not contain questions")
providerLog(logger.LevelInfo, "%v", err)
return err
}
if len(response.Questions) != len(response.Echos) {
err := fmt.Errorf("interactive auth error, http hook response questions don't match echos: %v %v",
len(response.Questions), len(response.Echos))
providerLog(logger.LevelInfo, "%v", err)
return err
}
return nil
}
func sendKeyboardAuthHTTPReq(url string, request keyboardAuthHookRequest) (keyboardAuthHookResponse, error) {
var response keyboardAuthHookResponse
func sendKeyboardAuthHTTPReq(url string, request *plugin.KeyboardAuthRequest) (*plugin.KeyboardAuthResponse, error) {
reqAsJSON, err := json.Marshal(request)
if err != nil {
providerLog(logger.LevelWarn, "error serializing keyboard interactive auth request: %v", err)
return response, err
return nil, err
}
resp, err := httpclient.Post(url, "application/json", bytes.NewBuffer(reqAsJSON))
if err != nil {
providerLog(logger.LevelWarn, "error getting keyboard interactive auth hook HTTP response: %v", err)
return response, err
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return response, fmt.Errorf("wrong keyboard interactive auth http status code: %v, expected 200", resp.StatusCode)
return nil, fmt.Errorf("wrong keyboard interactive auth http status code: %v, expected 200", resp.StatusCode)
}
var response plugin.KeyboardAuthResponse
err = render.DecodeJSON(resp.Body, &response)
return response, err
return &response, err
}
func executeKeyboardInteractivePlugin(user *User, client ssh.KeyboardInteractiveChallenge, ip, protocol string) (int, error) {
authResult := 0
requestID := xid.New().String()
authStep := 1
req := &plugin.KeyboardAuthRequest{
Username: user.Username,
IP: ip,
Password: user.Password,
RequestID: requestID,
Step: authStep,
}
var response *plugin.KeyboardAuthResponse
var err error
for {
response, err = plugin.Handler.ExecuteKeyboardInteractiveStep(req)
if err != nil {
return authResult, err
}
if response.AuthResult != 0 {
return response.AuthResult, err
}
if err = response.Validate(); err != nil {
providerLog(logger.LevelInfo, "invalid response from keyboard interactive plugin: %v", err)
return authResult, err
}
answers, err := getKeyboardInteractiveAnswers(client, response, user, ip, protocol)
if err != nil {
return authResult, err
}
authStep++
req = &plugin.KeyboardAuthRequest{
RequestID: requestID,
Step: authStep,
Username: user.Username,
Password: user.Password,
Answers: answers,
Questions: response.Questions,
}
}
}
func executeKeyboardInteractiveHTTPHook(user *User, authHook string, client ssh.KeyboardInteractiveChallenge, ip, protocol string) (int, error) {
authResult := 0
requestID := xid.New().String()
req := keyboardAuthHookRequest{
authStep := 1
req := &plugin.KeyboardAuthRequest{
Username: user.Username,
IP: ip,
Password: user.Password,
RequestID: requestID,
Step: authStep,
}
var response keyboardAuthHookResponse
var response *plugin.KeyboardAuthResponse
var err error
for {
response, err = sendKeyboardAuthHTTPReq(authHook, req)
@ -1878,15 +1913,18 @@ func executeKeyboardInteractiveHTTPHook(user *User, authHook string, client ssh.
if response.AuthResult != 0 {
return response.AuthResult, err
}
if err = validateKeyboardAuthResponse(response); err != nil {
if err = response.Validate(); err != nil {
providerLog(logger.LevelInfo, "invalid response from keyboard interactive http hook: %v", err)
return authResult, err
}
answers, err := getKeyboardInteractiveAnswers(client, response, user, ip, protocol)
if err != nil {
return authResult, err
}
req = keyboardAuthHookRequest{
authStep++
req = &plugin.KeyboardAuthRequest{
RequestID: requestID,
Step: authStep,
Username: user.Username,
Password: user.Password,
Answers: answers,
@ -1895,7 +1933,7 @@ func executeKeyboardInteractiveHTTPHook(user *User, authHook string, client ssh.
}
}
func getKeyboardInteractiveAnswers(client ssh.KeyboardInteractiveChallenge, response keyboardAuthHookResponse,
func getKeyboardInteractiveAnswers(client ssh.KeyboardInteractiveChallenge, response *plugin.KeyboardAuthResponse,
user *User, ip, protocol string) ([]string, error) {
questions := response.Questions
answers, err := client(user.Username, response.Instruction, questions, response.Echos)
@ -1920,7 +1958,7 @@ func getKeyboardInteractiveAnswers(client ssh.KeyboardInteractiveChallenge, resp
return answers, err
}
func handleProgramInteractiveQuestions(client ssh.KeyboardInteractiveChallenge, response keyboardAuthHookResponse,
func handleProgramInteractiveQuestions(client ssh.KeyboardInteractiveChallenge, response *plugin.KeyboardAuthResponse,
user *User, stdin io.WriteCloser, ip, protocol string) error {
answers, err := getKeyboardInteractiveAnswers(client, response, user, ip, protocol)
if err != nil {
@ -1964,7 +2002,7 @@ func executeKeyboardInteractiveProgram(user *User, authHook string, client ssh.K
var once sync.Once
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
var response keyboardAuthHookResponse
var response plugin.KeyboardAuthResponse
err = json.Unmarshal(scanner.Bytes(), &response)
if err != nil {
providerLog(logger.LevelInfo, "interactive auth error parsing response: %v", err)
@ -1975,12 +2013,13 @@ func executeKeyboardInteractiveProgram(user *User, authHook string, client ssh.K
authResult = response.AuthResult
break
}
if err = validateKeyboardAuthResponse(response); err != nil {
if err = response.Validate(); err != nil {
providerLog(logger.LevelInfo, "invalid response from keyboard interactive program: %v", err)
once.Do(func() { terminateInteractiveAuthProgram(cmd, false) })
break
}
go func() {
err := handleProgramInteractiveQuestions(client, response, user, stdin, ip, protocol)
err := handleProgramInteractiveQuestions(client, &response, user, stdin, ip, protocol)
if err != nil {
once.Do(func() { terminateInteractiveAuthProgram(cmd, false) })
}
@ -2001,7 +2040,9 @@ func executeKeyboardInteractiveProgram(user *User, authHook string, client ssh.K
func doKeyboardInteractiveAuth(user *User, authHook string, client ssh.KeyboardInteractiveChallenge, ip, protocol string) (User, error) {
var authResult int
var err error
if strings.HasPrefix(authHook, "http") {
if plugin.Handler.HasAuthScope(plugin.AuthScopeKeyboardInteractive) {
authResult, err = executeKeyboardInteractivePlugin(user, client, ip, protocol)
} else if strings.HasPrefix(authHook, "http") {
authResult, err = executeKeyboardInteractiveHTTPHook(user, authHook, client, ip, protocol)
} else {
authResult, err = executeKeyboardInteractiveProgram(user, authHook, client, ip, protocol)
@ -2389,6 +2430,67 @@ func doExternalAuth(username, password string, pubKey []byte, keyboardInteractiv
return provider.userExists(user.Username)
}
func doPluginAuth(username, password string, pubKey []byte, ip, protocol string,
tlsCert *x509.Certificate, authScope int,
) (User, error) {
var user User
u, userAsJSON, err := getUserAndJSONForHook(username)
if err != nil {
return user, err
}
if u.Filters.Hooks.ExternalAuthDisabled {
return u, nil
}
pkey, err := util.GetSSHPublicKeyAsString(pubKey)
if err != nil {
return user, err
}
startTime := time.Now()
out, err := plugin.Handler.Authenticate(username, password, ip, protocol, pkey, tlsCert, authScope, userAsJSON)
if err != nil {
return user, fmt.Errorf("plugin auth error for user %#v: %v, elapsed: %v, auth scope: %v",
username, err, time.Since(startTime), authScope)
}
providerLog(logger.LevelDebug, "plugin auth completed for user %#v, elapsed: %v,auth scope: %v",
username, time.Since(startTime), authScope)
if util.IsByteArrayEmpty(out) {
providerLog(logger.LevelDebug, "empty response from plugin auth, no modification requested for user %#v id: %v",
username, u.ID)
if u.ID == 0 {
return u, util.NewRecordNotFoundError(fmt.Sprintf("username %#v does not exist", username))
}
return u, nil
}
err = json.Unmarshal(out, &user)
if err != nil {
return user, fmt.Errorf("invalid plugin auth response: %v", err)
}
updateUserFromExtAuthResponse(&user, password, pkey)
if u.ID > 0 {
user.ID = u.ID
user.UsedQuotaSize = u.UsedQuotaSize
user.UsedQuotaFiles = u.UsedQuotaFiles
user.LastQuotaUpdate = u.LastQuotaUpdate
user.LastLogin = u.LastLogin
err = provider.updateUser(&user)
if err == nil {
webDAVUsersCache.swap(&user)
cachedPasswords.Add(user.Username, password)
}
return user, err
}
err = provider.addUser(&user)
if err != nil {
return user, err
}
return provider.userExists(user.Username)
}
func getUserAndJSONForHook(username string) (User, []byte, error) {
var userAsJSON []byte
u, err := provider.userExists(username)

View file

@ -251,7 +251,7 @@ The configuration file contains the following sections:
- `master_key`, string. Defines the master encryption key as string. If not empty, it takes precedence over `master_key_path`. Default empty.
- `master_key_path, string. Defines the absolute path to a file containing the master encryption key. Default empty.
- **plugins**, list of external plugins. Each plugin is configured using a struct with the following fields:
- `type`, string. Defines the plugin type. Supported types: `notifier`, `kms`.
- `type`, string. Defines the plugin type. Supported types: `notifier`, `kms`, `auth`.
- `notifier_options`, struct. Defines the options for notifier plugins.
- `fs_events`, list of strings. Defines the filesystem events that will be notified to this plugin.
- `user_events`, list of strings. Defines the user events that will be notified to this plugin.
@ -260,6 +260,8 @@ The configuration file contains the following sections:
- `kms_options`, struct. Defines the options for kms plugins.
- `scheme`, string. KMS scheme. Supported schemes are: `awskms`, `gcpkms`, `hashivault`, `azurekeyvault`.
- `encrypted_status`, string. Encrypted status for a KMS secret. Supported statuses are: `AWS`, `GCP`, `VaultTransit`, `AzureKeyVault`.
- `auth_options`, struct. Defines the options for auth plugins.
- `scope`, integer. 1 means passwords only. 2 means public keys only. 4 means key keyboard interactive only. 8 means TLS certificate. The flags can be combined, for example 6 means public keys and keyboard interactive. The scope must be explicit, `0` is not a valid option.
- `cmd`, string. Path to the plugin executable.
- `args`, list of strings. Optional arguments to pass to the plugin executable.
- `sha256sum`, string. SHA256 checksum for the plugin executable. If not empty it will be used to verify the integrity of the executable.

View file

@ -77,6 +77,7 @@ If the hook is an HTTP URL then it will be invoked as HTTP POST multiple times f
The request body will contain a JSON struct with the following fields:
- `request_id`, string. Unique request identifier
- `step`, integer. Counter starting from 1
- `username`, string
- `ip`, string
- `password`, string. This is the hashed password as stored inside the data provider
@ -95,7 +96,7 @@ Content-Length: 189
Content-Type: application/json
Accept-Encoding: gzip
{"request_id":"bq1r5r7cdrpd2qtn25ng","username":"a","ip":"127.0.0.1","password":"$pbkdf2-sha512$150000$ClOPkLNujMTL$XktKy0xuJsOfMYBz+f2bIyPTdbvDTSnJ1q+7+zp/HPq5Qojwp6kcpSIiVHiwvbi8P6HFXI/D3UJv9BLcnQFqPA=="}
{"request_id":"bq1r5r7cdrpd2qtn25ng","username":"a","ip":"127.0.0.1","step":1,"password":"$pbkdf2-sha512$150000$ClOPkLNujMTL$XktKy0xuJsOfMYBz+f2bIyPTdbvDTSnJ1q+7+zp/HPq5Qojwp6kcpSIiVHiwvbi8P6HFXI/D3UJv9BLcnQFqPA=="}
```
as you can see in this first requests `answers` and `questions` are null.
@ -123,7 +124,7 @@ Content-Length: 233
Content-Type: application/json
Accept-Encoding: gzip
{"request_id":"bq1r5r7cdrpd2qtn25ng","username":"a","ip":"127.0.0.1","password":"$pbkdf2-sha512$150000$ClOPkLNujMTL$XktKy0xuJsOfMYBz+f2bIyPTdbvDTSnJ1q+7+zp/HPq5Qojwp6kcpSIiVHiwvbi8P6HFXI/D3UJv9BLcnQFqPA==","answers":["OK"],"questions":["Password: "]}
{"request_id":"bq1r5r7cdrpd2qtn25ng","step":2,"username":"a","ip":"127.0.0.1","password":"$pbkdf2-sha512$150000$ClOPkLNujMTL$XktKy0xuJsOfMYBz+f2bIyPTdbvDTSnJ1q+7+zp/HPq5Qojwp6kcpSIiVHiwvbi8P6HFXI/D3UJv9BLcnQFqPA==","answers":["OK"],"questions":["Password: "]}
```
Here is the HTTP response that instructs SFTPGo to ask for a new question:
@ -149,7 +150,7 @@ Content-Length: 239
Content-Type: application/json
Accept-Encoding: gzip
{"request_id":"bq1r5r7cdrpd2qtn25ng","username":"a","ip":"127.0.0.1","password":"$pbkdf2-sha512$150000$ClOPkLNujMTL$XktKy0xuJsOfMYBz+f2bIyPTdbvDTSnJ1q+7+zp/HPq5Qojwp6kcpSIiVHiwvbi8P6HFXI/D3UJv9BLcnQFqPA==","answers":["answer2"],"questions":["Question2: "]}
{"request_id":"bq1r5r7cdrpd2qtn25ng","step":3,"username":"a","ip":"127.0.0.1","password":"$pbkdf2-sha512$150000$ClOPkLNujMTL$XktKy0xuJsOfMYBz+f2bIyPTdbvDTSnJ1q+7+zp/HPq5Qojwp6kcpSIiVHiwvbi8P6HFXI/D3UJv9BLcnQFqPA==","answers":["answer2"],"questions":["Question2: "]}
```
Here is the final HTTP response that allows the user login:

View file

@ -8,6 +8,12 @@ The plugins are configured via the `plugins` section in the main SFTPGo configur
For added security you can enable the automatic TLS. In this way, the client and the server automatically negotiate mutual TLS for transport authentication. This ensures that only the original client will be allowed to connect to the server, and all other connections will be rejected. The client will also refuse to connect to any server that isn't the original instance started by the client.
The following plugin types are supported:
- `auth`, allows to authenticate users
- `notifier`, allows to receive notifications for supported filesystem events such as file uploads, downloads etc. and user events such as add, update, delete.
- `kms`, allows to support additional KMS providers.
Full configuration details can be found [here](./full-configuration.md)
## Available plugins

180
sdk/plugin/auth.go Normal file
View file

@ -0,0 +1,180 @@
package plugin
import (
"crypto/sha256"
"errors"
"fmt"
"os/exec"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-plugin"
"github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/sdk/plugin/auth"
)
// Supported auth scopes
const (
AuthScopePassword = 1
AuthScopePublicKey = 2
AuthScopeKeyboardInteractive = 4
AuthScopeTLSCertificate = 8
)
// KeyboardAuthRequest defines the request for a keyboard interactive authentication step
type KeyboardAuthRequest struct {
RequestID string `json:"request_id"`
Step int `json:"step"`
Username string `json:"username,omitempty"`
IP string `json:"ip,omitempty"`
Password string `json:"password,omitempty"`
Answers []string `json:"answers,omitempty"`
Questions []string `json:"questions,omitempty"`
}
// KeyboardAuthResponse defines the response for a keyboard interactive authentication step
type KeyboardAuthResponse struct {
Instruction string `json:"instruction"`
Questions []string `json:"questions"`
Echos []bool `json:"echos"`
AuthResult int `json:"auth_result"`
CheckPwd int `json:"check_password"`
}
// Validate returns an error if the KeyboardAuthResponse is invalid
func (r *KeyboardAuthResponse) Validate() error {
if len(r.Questions) == 0 {
err := errors.New("interactive auth error: response does not contain questions")
return err
}
if len(r.Questions) != len(r.Echos) {
err := fmt.Errorf("interactive auth error: response questions don't match echos: %v %v",
len(r.Questions), len(r.Echos))
return err
}
return nil
}
// AuthConfig defines configuration parameters for auth plugins
type AuthConfig struct {
// Scope defines the scope for the authentication plugin.
// - 1 means passwords only
// - 2 means public keys only
// - 4 means keyboard interactive only
// - 8 means TLS certificates only
// you can combine the scopes, for example 3 means password and public key, 5 password and keyboard
// interactive and so on
Scope int `json:"scope" mapstructure:"scope"`
}
func (c *AuthConfig) validate() error {
authScopeMax := AuthScopePassword + AuthScopePublicKey + AuthScopeKeyboardInteractive + AuthScopeTLSCertificate
if c.Scope == 0 || c.Scope > authScopeMax {
return fmt.Errorf("invalid auth scope: %v", c.Scope)
}
return nil
}
type authPlugin struct {
config Config
service auth.Authenticator
client *plugin.Client
}
func newAuthPlugin(config Config) (*authPlugin, error) {
p := &authPlugin{
config: config,
}
if err := p.initialize(); err != nil {
logger.Warn(logSender, "", "unable to create auth plugin: %v, config %+v", err, config)
return nil, err
}
return p, nil
}
func (p *authPlugin) initialize() error {
killProcess(p.config.Cmd)
logger.Debug(logSender, "", "create new auth plugin %#v", p.config.Cmd)
if err := p.config.AuthOptions.validate(); err != nil {
return fmt.Errorf("invalid options for auth plugin %#v: %v", p.config.Cmd, err)
}
var secureConfig *plugin.SecureConfig
if p.config.SHA256Sum != "" {
secureConfig.Checksum = []byte(p.config.SHA256Sum)
secureConfig.Hash = sha256.New()
}
client := plugin.NewClient(&plugin.ClientConfig{
HandshakeConfig: auth.Handshake,
Plugins: auth.PluginMap,
Cmd: exec.Command(p.config.Cmd, p.config.Args...),
AllowedProtocols: []plugin.Protocol{
plugin.ProtocolGRPC,
},
AutoMTLS: p.config.AutoMTLS,
SecureConfig: secureConfig,
Managed: false,
Logger: &logger.HCLogAdapter{
Logger: hclog.New(&hclog.LoggerOptions{
Name: fmt.Sprintf("%v.%v", logSender, auth.PluginName),
Level: pluginsLogLevel,
DisableTime: true,
}),
},
})
rpcClient, err := client.Client()
if err != nil {
logger.Debug(logSender, "", "unable to get rpc client for kms plugin %#v: %v", p.config.Cmd, err)
return err
}
raw, err := rpcClient.Dispense(auth.PluginName)
if err != nil {
logger.Debug(logSender, "", "unable to get plugin %v from rpc client for command %#v: %v",
auth.PluginName, p.config.Cmd, err)
return err
}
p.service = raw.(auth.Authenticator)
p.client = client
return nil
}
func (p *authPlugin) exited() bool {
return p.client.Exited()
}
func (p *authPlugin) cleanup() {
p.client.Kill()
}
func (p *authPlugin) checkUserAndPass(username, password, ip, protocol string, userAsJSON []byte) ([]byte, error) {
return p.service.CheckUserAndPass(username, password, ip, protocol, userAsJSON)
}
func (p *authPlugin) checkUserAndTLSCertificate(username, tlsCert, ip, protocol string, userAsJSON []byte) ([]byte, error) {
return p.service.CheckUserAndTLSCert(username, tlsCert, ip, protocol, userAsJSON)
}
func (p *authPlugin) checkUserAndPublicKey(username, pubKey, ip, protocol string, userAsJSON []byte) ([]byte, error) {
return p.service.CheckUserAndPublicKey(username, pubKey, ip, protocol, userAsJSON)
}
func (p *authPlugin) checkUserAndKeyboardInteractive(username, ip, protocol string, userAsJSON []byte) ([]byte, error) {
return p.service.CheckUserAndKeyboardInteractive(username, ip, protocol, userAsJSON)
}
func (p *authPlugin) sendKeyboardIteractiveRequest(req *KeyboardAuthRequest) (*KeyboardAuthResponse, error) {
instructions, questions, echos, authResult, checkPassword, err := p.service.SendKeyboardAuthRequest(
req.RequestID, req.Username, req.Password, req.IP, req.Answers, req.Questions, int32(req.Step))
if err != nil {
return nil, err
}
return &KeyboardAuthResponse{
Instruction: instructions,
Questions: questions,
Echos: echos,
AuthResult: authResult,
CheckPwd: checkPassword,
}, nil
}

59
sdk/plugin/auth/auth.go Normal file
View file

@ -0,0 +1,59 @@
// Package auth defines the implementation for authentication plugins.
// Authentication plugins allow to authenticate external users
package auth
import (
"context"
"github.com/hashicorp/go-plugin"
"google.golang.org/grpc"
"github.com/drakkan/sftpgo/v2/sdk/plugin/auth/proto"
)
const (
// PluginName defines the name for a notifier plugin
PluginName = "auth"
)
// Handshake is a common handshake that is shared by plugin and host.
var Handshake = plugin.HandshakeConfig{
ProtocolVersion: 1,
MagicCookieKey: "SFTPGO_AUTH_PLUGIN",
MagicCookieValue: "d1ed507d-d2be-4a38-a460-6fe0b2cc7efc",
}
// PluginMap is the map of plugins we can dispense.
var PluginMap = map[string]plugin.Plugin{
PluginName: &Plugin{},
}
// Authenticator defines the interface for authentication plugins
type Authenticator interface {
CheckUserAndPass(username, password, ip, protocol string, userAsJSON []byte) ([]byte, error)
CheckUserAndTLSCert(username, tlsCert, ip, protocol string, userAsJSON []byte) ([]byte, error)
CheckUserAndPublicKey(username, pubKey, ip, protocol string, userAsJSON []byte) ([]byte, error)
CheckUserAndKeyboardInteractive(username, ip, protocol string, userAsJSON []byte) ([]byte, error)
SendKeyboardAuthRequest(requestID, username, password, ip string, answers, questions []string, step int32) (string, []string, []bool, int, int, error)
}
// Plugin defines the implementation to serve/connect to an authe plugin
type Plugin struct {
plugin.Plugin
Impl Authenticator
}
// GRPCServer defines the GRPC server implementation for this plugin
func (p *Plugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error {
proto.RegisterAuthServer(s, &GRPCServer{
Impl: p.Impl,
})
return nil
}
// GRPCClient defines the GRPC client implementation for this plugin
func (p *Plugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) {
return &GRPCClient{
client: proto.NewAuthClient(c),
}, nil
}

165
sdk/plugin/auth/grpc.go Normal file
View file

@ -0,0 +1,165 @@
package auth
import (
"context"
"time"
"github.com/drakkan/sftpgo/v2/sdk/plugin/auth/proto"
)
const (
rpcTimeout = 20 * time.Second
)
// GRPCClient is an implementation of Authenticator interface that talks over RPC.
type GRPCClient struct {
client proto.AuthClient
}
// CheckUserAndPass implements the Authenticator interface
func (c *GRPCClient) CheckUserAndPass(username, password, ip, protocol string, userAsJSON []byte) ([]byte, error) {
ctx, cancel := context.WithTimeout(context.Background(), rpcTimeout)
defer cancel()
resp, err := c.client.CheckUserAndPass(ctx, &proto.CheckUserAndPassRequest{
Username: username,
Password: password,
Ip: ip,
Protocol: protocol,
User: userAsJSON,
})
if err != nil {
return nil, err
}
return resp.User, nil
}
// CheckUserAndTLSCert implements the Authenticator interface
func (c *GRPCClient) CheckUserAndTLSCert(username, tlsCert, ip, protocol string, userAsJSON []byte) ([]byte, error) {
ctx, cancel := context.WithTimeout(context.Background(), rpcTimeout)
defer cancel()
resp, err := c.client.CheckUserAndTLSCert(ctx, &proto.CheckUserAndTLSCertRequest{
Username: username,
TlsCert: tlsCert,
Ip: ip,
Protocol: protocol,
User: userAsJSON,
})
if err != nil {
return nil, err
}
return resp.User, nil
}
// CheckUserAndPublicKey implements the Authenticator interface
func (c *GRPCClient) CheckUserAndPublicKey(username, pubKey, ip, protocol string, userAsJSON []byte) ([]byte, error) {
ctx, cancel := context.WithTimeout(context.Background(), rpcTimeout)
defer cancel()
resp, err := c.client.CheckUserAndPublicKey(ctx, &proto.CheckUserAndPublicKeyRequest{
Username: username,
PubKey: pubKey,
Ip: ip,
Protocol: protocol,
User: userAsJSON,
})
if err != nil {
return nil, err
}
return resp.User, nil
}
// CheckUserAndKeyboardInteractive implements the Authenticator interface
func (c *GRPCClient) CheckUserAndKeyboardInteractive(username, ip, protocol string, userAsJSON []byte) ([]byte, error) {
ctx, cancel := context.WithTimeout(context.Background(), rpcTimeout)
defer cancel()
resp, err := c.client.CheckUserAndKeyboardInteractive(ctx, &proto.CheckUserAndKeyboardInteractiveRequest{
Username: username,
Ip: ip,
Protocol: protocol,
User: userAsJSON,
})
if err != nil {
return nil, err
}
return resp.User, nil
}
// SendKeyboardAuthRequest implements the Authenticator interface
func (c *GRPCClient) SendKeyboardAuthRequest(requestID, username, password, ip string, answers, questions []string, step int32) (string, []string, []bool, int, int, error) {
ctx, cancel := context.WithTimeout(context.Background(), rpcTimeout)
defer cancel()
resp, err := c.client.SendKeyboardAuthRequest(ctx, &proto.KeyboardAuthRequest{
RequestID: requestID,
Username: username,
Password: password,
Ip: ip,
Answers: answers,
Questions: questions,
Step: step,
})
if err != nil {
return "", nil, nil, 0, 0, err
}
return resp.Instructions, resp.Questions, resp.Echos, int(resp.AuthResult), int(resp.CheckPassword), err
}
// GRPCServer defines the gRPC server that GRPCClient talks to.
type GRPCServer struct {
Impl Authenticator
}
// CheckUserAndPass implements the server side check user and password method
func (s *GRPCServer) CheckUserAndPass(ctx context.Context, req *proto.CheckUserAndPassRequest) (*proto.AuthResponse, error) {
user, err := s.Impl.CheckUserAndPass(req.Username, req.Password, req.Ip, req.Protocol, req.User)
if err != nil {
return nil, err
}
return &proto.AuthResponse{User: user}, nil
}
// CheckUserAndTLSCert implements the server side check user and tls certificate method
func (s *GRPCServer) CheckUserAndTLSCert(ctx context.Context, req *proto.CheckUserAndTLSCertRequest) (*proto.AuthResponse, error) {
user, err := s.Impl.CheckUserAndTLSCert(req.Username, req.TlsCert, req.Ip, req.Protocol, req.User)
if err != nil {
return nil, err
}
return &proto.AuthResponse{User: user}, nil
}
// CheckUserAndPublicKey implements the server side check user and public key method
func (s *GRPCServer) CheckUserAndPublicKey(ctx context.Context, req *proto.CheckUserAndPublicKeyRequest) (*proto.AuthResponse, error) {
user, err := s.Impl.CheckUserAndPublicKey(req.Username, req.PubKey, req.Ip, req.Protocol, req.User)
if err != nil {
return nil, err
}
return &proto.AuthResponse{User: user}, nil
}
// CheckUserAndKeyboardInteractive implements the server side check user and keyboard interactive method
func (s *GRPCServer) CheckUserAndKeyboardInteractive(ctx context.Context, req *proto.CheckUserAndKeyboardInteractiveRequest) (*proto.AuthResponse, error) {
user, err := s.Impl.CheckUserAndKeyboardInteractive(req.Username, req.Ip, req.Protocol, req.User)
if err != nil {
return nil, err
}
return &proto.AuthResponse{User: user}, nil
}
// SendKeyboardAuthRequest implements the server side method to send a keyboard interactive authentication request
func (s *GRPCServer) SendKeyboardAuthRequest(ctx context.Context, req *proto.KeyboardAuthRequest) (*proto.KeyboardAuthResponse, error) {
instructions, questions, echos, authResult, checkPwd, err := s.Impl.SendKeyboardAuthRequest(req.RequestID, req.Username,
req.Password, req.Ip, req.Answers, req.Questions, req.Step)
if err != nil {
return nil, err
}
return &proto.KeyboardAuthResponse{
Instructions: instructions,
Questions: questions,
Echos: echos,
AuthResult: int32(authResult),
CheckPassword: int32(checkPwd),
}, nil
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,65 @@
syntax = "proto3";
package proto;
option go_package = "sdk/plugin/auth/proto";
message CheckUserAndPassRequest {
string username = 1;
string password = 2;
string ip = 3;
string protocol = 4;
bytes user = 5; // SFTPGo user JSON serialized
}
message CheckUserAndTLSCertRequest {
string username = 1;
string tlsCert = 2; // tls certificate pem encoded
string ip = 3;
string protocol = 4;
bytes user = 5; // SFTPGo user JSON serialized
}
message CheckUserAndPublicKeyRequest {
string username = 1;
string pubKey = 2;
string ip = 3;
string protocol = 4;
bytes user = 5; // SFTPGo user JSON serialized
}
message CheckUserAndKeyboardInteractiveRequest {
string username = 1;
string ip = 2;
string protocol = 3;
bytes user = 4; // SFTPGo user JSON serialized
}
message KeyboardAuthRequest {
string requestID = 1;
string username = 2;
string password = 3;
string ip = 4;
repeated string answers = 5;
repeated string questions = 6;
int32 step = 7;
}
message KeyboardAuthResponse {
string instructions = 1;
repeated string questions = 2;
repeated bool echos = 3;
int32 auth_result = 4;
int32 check_password = 5;
}
message AuthResponse {
bytes user = 1; // SFTPGo user JSON serialized
}
service Auth {
rpc CheckUserAndPass(CheckUserAndPassRequest) returns (AuthResponse);
rpc CheckUserAndTLSCert(CheckUserAndTLSCertRequest) returns (AuthResponse);
rpc CheckUserAndPublicKey(CheckUserAndPublicKeyRequest) returns (AuthResponse);
rpc CheckUserAndKeyboardInteractive(CheckUserAndKeyboardInteractiveRequest) returns (AuthResponse);
rpc SendKeyboardAuthRequest(KeyboardAuthRequest) returns (KeyboardAuthResponse);
}

View file

@ -27,7 +27,7 @@ type KMSConfig struct {
EncryptedStatus string `json:"encrypted_status" mapstructure:"encrypted_status"`
}
func (c *KMSConfig) isValid() error {
func (c *KMSConfig) validate() error {
if !util.IsStringInSlice(c.Scheme, validKMSSchemes) {
return fmt.Errorf("invalid kms scheme: %v", c.Scheme)
}
@ -57,7 +57,7 @@ func newKMSPlugin(config Config) (*kmsPlugin, error) {
func (p *kmsPlugin) initialize() error {
killProcess(p.config.Cmd)
logger.Debug(logSender, "", "create new kms plugin %#v", p.config.Cmd)
if err := p.config.KMSOptions.isValid(); err != nil {
if err := p.config.KMSOptions.validate(); err != nil {
return fmt.Errorf("invalid options for kms plugin %#v: %v", p.config.Cmd, err)
}
var secureConfig *plugin.SecureConfig

View file

@ -2,5 +2,6 @@
protoc notifier/proto/notifier.proto --go_out=plugins=grpc:../.. --go_out=../../..
protoc kms/proto/kms.proto --go_out=plugins=grpc:../.. --go_out=../../..
protoc auth/proto/auth.proto --go_out=plugins=grpc:../.. --go_out=../../..

View file

@ -1,6 +1,7 @@
// Package notifier defines the implementation for event notifier plugins.
// Notifier plugins allow to receive filesystem events such as file uploads,
// downloads etc. and user events such as add, update, delete.
// Notifier plugins allow to receive notifications for supported filesystem
// events such as file uploads, downloads etc. and user events such as add,
// update, delete.
package notifier
import (

View file

@ -144,7 +144,7 @@ type UserEvent struct {
Timestamp *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
Action string `protobuf:"bytes,2,opt,name=action,proto3" json:"action,omitempty"`
User []byte `protobuf:"bytes,3,opt,name=user,proto3" json:"user,omitempty"` // SFTPGo user json serialized
User []byte `protobuf:"bytes,3,opt,name=user,proto3" json:"user,omitempty"` // SFTPGo user JSON serialized
}
func (x *UserEvent) Reset() {

View file

@ -21,7 +21,7 @@ message FsEvent {
message UserEvent {
google.protobuf.Timestamp timestamp = 1;
string action = 2;
bytes user = 3; // SFTPGo user json serialized
bytes user = 3; // SFTPGo user JSON serialized
}
service Notifier {

View file

@ -2,6 +2,8 @@
package plugin
import (
"crypto/x509"
"errors"
"fmt"
"sync"
"sync/atomic"
@ -11,8 +13,10 @@ import (
"github.com/drakkan/sftpgo/v2/kms"
"github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/sdk/plugin/auth"
kmsplugin "github.com/drakkan/sftpgo/v2/sdk/plugin/kms"
"github.com/drakkan/sftpgo/v2/sdk/plugin/notifier"
"github.com/drakkan/sftpgo/v2/util"
)
const (
@ -38,6 +42,8 @@ type Config struct {
NotifierOptions NotifierConfig `json:"notifier_options" mapstructure:"notifier_options"`
// KMSOptions defines options for a KMS plugin
KMSOptions KMSConfig `json:"kms_options" mapstructure:"kms_options"`
// AuthOptions defines options for authentication plugins
AuthOptions AuthConfig `json:"auth_options" mapstructure:"auth_options"`
// Path to the plugin executable
Cmd string `json:"cmd" mapstructure:"cmd"`
// Args to pass to the plugin executable
@ -69,19 +75,23 @@ type Manager struct {
closed int32
done chan bool
// List of configured plugins
Configs []Config `json:"plugins" mapstructure:"plugins"`
notifLock sync.RWMutex
notifiers []*notifierPlugin
kmsLock sync.RWMutex
kms []*kmsPlugin
Configs []Config `json:"plugins" mapstructure:"plugins"`
notifLock sync.RWMutex
notifiers []*notifierPlugin
kmsLock sync.RWMutex
kms []*kmsPlugin
authLock sync.RWMutex
auths []*authPlugin
authScopes int
}
// Initialize initializes the configured plugins
func Initialize(configs []Config, logVerbose bool) error {
Handler = Manager{
Configs: configs,
done: make(chan bool),
closed: 0,
Configs: configs,
done: make(chan bool),
closed: 0,
authScopes: -1,
}
if len(configs) == 0 {
return nil
@ -117,6 +127,17 @@ func Initialize(configs []Config, logVerbose bool) error {
Handler.Configs[idx].newKMSPluginSecretProvider)
logger.Debug(logSender, "", "registered secret provider for scheme: %v, encrypted status: %v",
config.KMSOptions.Scheme, config.KMSOptions.EncryptedStatus)
case auth.PluginName:
plugin, err := newAuthPlugin(config)
if err != nil {
return err
}
Handler.auths = append(Handler.auths, plugin)
if Handler.authScopes == -1 {
Handler.authScopes = config.AuthOptions.Scope
} else {
Handler.authScopes |= config.AuthOptions.Scope
}
default:
return fmt.Errorf("unsupported plugin type: %v", config.Type)
}
@ -181,6 +202,133 @@ func (m *Manager) kmsDecrypt(secret kms.BaseSecret, url string, masterKey string
return plugin.Decrypt(secret, url, masterKey)
}
// HasAuthScope returns true if there is an auth plugin that support the specified scope
func (m *Manager) HasAuthScope(scope int) bool {
if m.authScopes == -1 {
return false
}
return m.authScopes&scope != 0
}
// Authenticate tries to authenticate the specified user using an external plugin
func (m *Manager) Authenticate(username, password, ip, protocol string, pkey string,
tlsCert *x509.Certificate, authScope int, userAsJSON []byte,
) ([]byte, error) {
switch authScope {
case AuthScopePassword:
return m.checkUserAndPass(username, password, ip, protocol, userAsJSON)
case AuthScopePublicKey:
return m.checkUserAndPublicKey(username, pkey, ip, protocol, userAsJSON)
case AuthScopeKeyboardInteractive:
return m.checkUserAndKeyboardInteractive(username, ip, protocol, userAsJSON)
case AuthScopeTLSCertificate:
cert, err := util.EncodeTLSCertToPem(tlsCert)
if err != nil {
logger.Warn(logSender, "", "unable to encode tls certificate to pem: %v", err)
return nil, fmt.Errorf("unable to encode tls cert to pem: %w", err)
}
return m.checkUserAndTLSCert(username, cert, ip, protocol, userAsJSON)
default:
return nil, fmt.Errorf("unsupported auth scope: %v", authScope)
}
}
// ExecuteKeyboardInteractiveStep executes a keyboard interactive step
func (m *Manager) ExecuteKeyboardInteractiveStep(req *KeyboardAuthRequest) (*KeyboardAuthResponse, error) {
var plugin *authPlugin
m.authLock.Lock()
for _, p := range m.auths {
if p.config.AuthOptions.Scope&AuthScopePassword != 0 {
plugin = p
break
}
}
m.authLock.Unlock()
if plugin == nil {
return nil, errors.New("no auth plugin configured for keyaboard interactive authentication step")
}
return plugin.sendKeyboardIteractiveRequest(req)
}
func (m *Manager) checkUserAndPass(username, password, ip, protocol string, userAsJSON []byte) ([]byte, error) {
var plugin *authPlugin
m.authLock.Lock()
for _, p := range m.auths {
if p.config.AuthOptions.Scope&AuthScopePassword != 0 {
plugin = p
break
}
}
m.authLock.Unlock()
if plugin == nil {
return nil, errors.New("no auth plugin configured for password checking")
}
return plugin.checkUserAndPass(username, password, ip, protocol, userAsJSON)
}
func (m *Manager) checkUserAndPublicKey(username, pubKey, ip, protocol string, userAsJSON []byte) ([]byte, error) {
var plugin *authPlugin
m.authLock.Lock()
for _, p := range m.auths {
if p.config.AuthOptions.Scope&AuthScopePublicKey != 0 {
plugin = p
break
}
}
m.authLock.Unlock()
if plugin == nil {
return nil, errors.New("no auth plugin configured for public key checking")
}
return plugin.checkUserAndPublicKey(username, pubKey, ip, protocol, userAsJSON)
}
func (m *Manager) checkUserAndTLSCert(username, tlsCert, ip, protocol string, userAsJSON []byte) ([]byte, error) {
var plugin *authPlugin
m.authLock.Lock()
for _, p := range m.auths {
if p.config.AuthOptions.Scope&AuthScopeTLSCertificate != 0 {
plugin = p
break
}
}
m.authLock.Unlock()
if plugin == nil {
return nil, errors.New("no auth plugin configured for TLS certificate checking")
}
return plugin.checkUserAndTLSCertificate(username, tlsCert, ip, protocol, userAsJSON)
}
func (m *Manager) checkUserAndKeyboardInteractive(username, ip, protocol string, userAsJSON []byte) ([]byte, error) {
var plugin *authPlugin
m.authLock.Lock()
for _, p := range m.auths {
if p.config.AuthOptions.Scope&AuthScopeKeyboardInteractive != 0 {
plugin = p
break
}
}
m.authLock.Unlock()
if plugin == nil {
return nil, errors.New("no auth plugin configured for keyboard interactive checking")
}
return plugin.checkUserAndKeyboardInteractive(username, ip, protocol, userAsJSON)
}
func (m *Manager) checkCrashedPlugins() {
m.notifLock.RLock()
for idx, n := range m.notifiers {
@ -203,6 +351,16 @@ func (m *Manager) checkCrashedPlugins() {
}
}
m.kmsLock.RUnlock()
m.authLock.RLock()
for idx, a := range m.auths {
if a.exited() {
defer func(cfg Config, index int) {
Handler.restartAuthPlugin(cfg, index)
}(a.config, idx)
}
}
m.authLock.RUnlock()
}
func (m *Manager) restartNotifierPlugin(config Config, idx int) {
@ -239,6 +397,22 @@ func (m *Manager) restartKMSPlugin(config Config, idx int) {
m.kmsLock.Unlock()
}
func (m *Manager) restartAuthPlugin(config Config, idx int) {
if atomic.LoadInt32(&m.closed) == 1 {
return
}
logger.Info(logSender, "", "try to restart crashed auth plugin %#v, idx: %v", config.Cmd, idx)
plugin, err := newAuthPlugin(config)
if err != nil {
logger.Warn(logSender, "", "unable to restart auth plugin %#v, err: %v", config.Cmd, err)
return
}
m.authLock.Lock()
m.auths[idx] = plugin
m.authLock.Unlock()
}
// Cleanup releases all the active plugins
func (m *Manager) Cleanup() {
atomic.StoreInt32(&m.closed, 1)
@ -256,6 +430,13 @@ func (m *Manager) Cleanup() {
k.cleanup()
}
m.kmsLock.Unlock()
m.authLock.Lock()
for _, a := range m.auths {
logger.Debug(logSender, "", "cleanup auth plugin %v", a.config.Cmd)
a.cleanup()
}
m.authLock.Unlock()
}
func startCheckTicker() {

View file

@ -21,6 +21,7 @@ import (
"github.com/drakkan/sftpgo/v2/dataprovider"
"github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/metric"
"github.com/drakkan/sftpgo/v2/sdk/plugin"
"github.com/drakkan/sftpgo/v2/util"
"github.com/drakkan/sftpgo/v2/vfs"
)
@ -306,22 +307,24 @@ func (c *Configuration) configureLoginBanner(serverConfig *ssh.ServerConfig, con
}
func (c *Configuration) configureKeyboardInteractiveAuth(serverConfig *ssh.ServerConfig) {
if c.KeyboardInteractiveHook == "" {
if c.KeyboardInteractiveHook == "" && !plugin.Handler.HasAuthScope(plugin.AuthScopeKeyboardInteractive) {
return
}
if !strings.HasPrefix(c.KeyboardInteractiveHook, "http") {
if !filepath.IsAbs(c.KeyboardInteractiveHook) {
logger.WarnToConsole("invalid keyboard interactive authentication program: %#v must be an absolute path",
c.KeyboardInteractiveHook)
logger.Warn(logSender, "", "invalid keyboard interactive authentication program: %#v must be an absolute path",
c.KeyboardInteractiveHook)
return
}
_, err := os.Stat(c.KeyboardInteractiveHook)
if err != nil {
logger.WarnToConsole("invalid keyboard interactive authentication program:: %v", err)
logger.Warn(logSender, "", "invalid keyboard interactive authentication program:: %v", err)
return
if c.KeyboardInteractiveHook != "" {
if !strings.HasPrefix(c.KeyboardInteractiveHook, "http") {
if !filepath.IsAbs(c.KeyboardInteractiveHook) {
logger.WarnToConsole("invalid keyboard interactive authentication program: %#v must be an absolute path",
c.KeyboardInteractiveHook)
logger.Warn(logSender, "", "invalid keyboard interactive authentication program: %#v must be an absolute path",
c.KeyboardInteractiveHook)
return
}
_, err := os.Stat(c.KeyboardInteractiveHook)
if err != nil {
logger.WarnToConsole("invalid keyboard interactive authentication program:: %v", err)
logger.Warn(logSender, "", "invalid keyboard interactive authentication program:: %v", err)
return
}
}
}
serverConfig.KeyboardInteractiveCallback = func(conn ssh.ConnMetadata, client ssh.KeyboardInteractiveChallenge) (*ssh.Permissions, error) {