diff --git a/README.md b/README.md index 19f2fcb9..0a885000 100644 --- a/README.md +++ b/README.md @@ -410,6 +410,7 @@ The program must write the questions on its standard output, in a single line, u - `instruction`, string. A short description to show to the user that is trying to authenticate. Can be empty or omitted - `questions`, list of questions to be asked to the user - `echos` list of boolean flags corresponding to the questions (so the lengths of both lists must be the same) and indicating whether user's reply for a particular question should be echoed on the screen while they are typing: true if it should be echoed, or false if it should be hidden. +- `check_password` optional integer. Ask exactly one question and set this field to 1 if the expected answer is the user password and you want that SFTPGo check it for you. If the password is correct the returned response to the program is `OK`. If the password is wrong the program will be terminated and an authentication error will be returned to the user - `auth_result`, integer. Set this field to 1 to indicate successful authentication, 0 is ignored, any other value means authentication error. If this fields is found and it is different from 0 then SFTPGo does not read any other questions from the external program and finalize the authentication. SFTPGo writes the user answers to the program standard input, one per line, in the same order of the questions. @@ -439,6 +440,30 @@ else fi ``` +and here is an example where SFTPGo check the user password for you: + +``` +#!/bin/sh + +echo '{"questions":["Password: "],"instruction":"This is a sample for keyboard interactive authentication","echos":[false],"check_password":1}' + +read ANSWER1 + +if test "$ANSWER1" != "OK"; then + exit 1 +fi + +echo '{"questions":["One time token: "],"instruction":"","echos":[false]}' + +read ANSWER2 + +if test "$ANSWER2" = "token"; then + echo '{"auth_result":1}' +else + echo '{"auth_result":-1}' +fi +``` + ## Custom Actions SFTPGo allows to configure custom commands and/or HTTP notifications on file upload, download, delete, rename, on SSH commands and on user add, update and delete. @@ -659,8 +684,8 @@ For each account the following properties can be configured: - `fs_provider`, filesystem to serve via SFTP. Local filesystem and S3 Compatible Object Storage are supported - `s3_bucket`, required for S3 filesystem - `s3_region`, required for S3 filesystem. Must match the region for your bucket. You can find here the list of available [AWS regions](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html#concepts-available-regions). For example if your bucket is at `Frankfurt` you have to set the region to `eu-central-1` -- `s3_access_key`, required for S3 filesystem -- `s3_access_secret`, required for S3 filesystem. It is stored encrypted (AES-256-GCM) +- `s3_access_key` +- `s3_access_secret`, if provided it is stored encrypted (AES-256-GCM) - `s3_endpoint`, specifies a S3 endpoint (server) different from AWS. It is not required if you are connecting to AWS - `s3_storage_class`, leave blank to use the default or specify a valid AWS [storage class](https://docs.aws.amazon.com/AmazonS3/latest/dev/storage-class-intro.html) - `s3_key_prefix`, allows to restrict access to the virtual folder identified by this prefix and its contents diff --git a/dataprovider/dataprovider.go b/dataprovider/dataprovider.go index d487ccbf..48d6c0d2 100644 --- a/dataprovider/dataprovider.go +++ b/dataprovider/dataprovider.go @@ -16,6 +16,7 @@ import ( "errors" "fmt" "hash" + "io" "io/ioutil" "net" "net/http" @@ -203,6 +204,7 @@ type keyboardAuthProgramResponse struct { Questions []string `json:"questions"` Echos []bool `json:"echos"` AuthResult int `json:"auth_result"` + CheckPwd int `json:"check_password"` } // ValidationError raised if input data is not valid @@ -894,6 +896,42 @@ func terminateInteractiveAuthProgram(cmd *exec.Cmd, isFinished bool) { cmd.Process.Kill() } +func handleInteractiveQuestions(client ssh.KeyboardInteractiveChallenge, response keyboardAuthProgramResponse, + user User, stdin io.WriteCloser) error { + questions := response.Questions + answers, err := client(user.Username, response.Instruction, questions, response.Echos) + if err != nil { + providerLog(logger.LevelInfo, "error getting interactive auth client response: %v", err) + return err + } + if len(answers) != len(questions) { + err = fmt.Errorf("client answers does not match questions, expected: %v actual: %v", questions, answers) + providerLog(logger.LevelInfo, "keyboard interactive auth error: %v", err) + return err + } + if len(answers) == 1 && response.CheckPwd > 0 { + _, err = checkUserAndPass(user, answers[0]) + providerLog(logger.LevelInfo, "interactive auth program requested password validation for user %#v, validation error: %v", + user.Username, err) + if err != nil { + return err + } + answers[0] = "OK" + } + for _, answer := range answers { + if runtime.GOOS == "windows" { + answer += "\r" + } + answer += "\n" + _, err = stdin.Write([]byte(answer)) + if err != nil { + providerLog(logger.LevelError, "unable to write client answer to keyboard interactive program: %v", err) + return err + } + } + return nil +} + func doKeyboardInteractiveAuth(user User, authProgram string, client ssh.KeyboardInteractiveChallenge) (User, error) { ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() @@ -940,29 +978,9 @@ func doKeyboardInteractiveAuth(user User, authProgram string, client ssh.Keyboar break } go func() { - questions := response.Questions - answers, err := client(user.Username, response.Instruction, questions, response.Echos) + err := handleInteractiveQuestions(client, response, user, stdin) if err != nil { - providerLog(logger.LevelInfo, "error getting interactive auth client response: %v", err) once.Do(func() { terminateInteractiveAuthProgram(cmd, false) }) - return - } - if len(answers) != len(questions) { - providerLog(logger.LevelInfo, "client answers does not match questions, expected: %v actual: %v", questions, answers) - once.Do(func() { terminateInteractiveAuthProgram(cmd, false) }) - return - } - for _, answer := range answers { - if runtime.GOOS == "windows" { - answer += "\r" - } - answer += "\n" - _, err = stdin.Write([]byte(answer)) - if err != nil { - providerLog(logger.LevelError, "unable to write client answer to keyboard interactive program: %v", err) - once.Do(func() { terminateInteractiveAuthProgram(cmd, false) }) - return - } } }() }