keyboard interactive auth: allows to automatically check the user password

This simplify the common pattern where the user password and a one time
token is requested: now the external program can delegate password check
to SFTPGo and verify the token itself
This commit is contained in:
Nicola Murino 2020-02-16 11:43:52 +01:00
parent 58253968fc
commit 79c8b6cbc2
2 changed files with 66 additions and 23 deletions

View file

@ -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

View file

@ -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
}
}
}()
}