mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-29 02:50:29 +00:00
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
|
- `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
|
- `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.
|
- `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.
|
- `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.
|
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
|
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
|
## 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.
|
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
|
- `fs_provider`, filesystem to serve via SFTP. Local filesystem and S3 Compatible Object Storage are supported
|
||||||
- `s3_bucket`, required for S3 filesystem
|
- `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_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_key`
|
||||||
- `s3_access_secret`, required for S3 filesystem. It is stored encrypted (AES-256-GCM)
|
- `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_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_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
|
- `s3_key_prefix`, allows to restrict access to the virtual folder identified by this prefix and its contents
|
||||||
|
|
|
@ -16,6 +16,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"hash"
|
"hash"
|
||||||
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -203,6 +204,7 @@ type keyboardAuthProgramResponse struct {
|
||||||
Questions []string `json:"questions"`
|
Questions []string `json:"questions"`
|
||||||
Echos []bool `json:"echos"`
|
Echos []bool `json:"echos"`
|
||||||
AuthResult int `json:"auth_result"`
|
AuthResult int `json:"auth_result"`
|
||||||
|
CheckPwd int `json:"check_password"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidationError raised if input data is not valid
|
// ValidationError raised if input data is not valid
|
||||||
|
@ -894,6 +896,42 @@ func terminateInteractiveAuthProgram(cmd *exec.Cmd, isFinished bool) {
|
||||||
cmd.Process.Kill()
|
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) {
|
func doKeyboardInteractiveAuth(user User, authProgram string, client ssh.KeyboardInteractiveChallenge) (User, error) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
@ -940,29 +978,9 @@ func doKeyboardInteractiveAuth(user User, authProgram string, client ssh.Keyboar
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
questions := response.Questions
|
err := handleInteractiveQuestions(client, response, user, stdin)
|
||||||
answers, err := client(user.Username, response.Instruction, questions, response.Echos)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
providerLog(logger.LevelInfo, "error getting interactive auth client response: %v", err)
|
|
||||||
once.Do(func() { terminateInteractiveAuthProgram(cmd, false) })
|
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