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:
parent
58253968fc
commit
79c8b6cbc2
2 changed files with 66 additions and 23 deletions
29
README.md
29
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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue