forked from drakkan/sftpgo
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add some examples hooks for one time password logins
The examples use Twillo Authy since I use it for my GitHub account. You can easily use other multi factor authentication software in a similar way.
- Loading branch information
Showing
10 changed files
with
322 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
# Authy | ||
|
||
These example show how-to integrate [Twillo Authy API](https://www.twilio.com/docs/authy/api) for One-Time-Password logins. | ||
|
||
The examples assume that the user has the free [Authy app](https://authy.com/) installed and uses it to generate offline [TOTP](https://en.wikipedia.org/wiki/Time-based_One-time_Password_algorithm) codes (soft tokens). | ||
|
||
You first need to [create an Authy Application in the Twilio Console](https://twilio.com/console/authy/applications?_ga=2.205553366.451688189.1597667213-1526360003.1597667213), then you can create a new Authy user and store a reference to the matching SFTPGo account. | ||
|
||
Verify that your Authy application is successfully registered: | ||
|
||
```bash | ||
export AUTHY_API_KEY=<your api key here> | ||
curl 'https://api.authy.com/protected/json/app/details' -H "X-Authy-API-Key: $AUTHY_API_KEY" | ||
``` | ||
|
||
now create an Authy user: | ||
|
||
```bash | ||
curl -XPOST "https://api.authy.com/protected/json/users/new" \ | ||
-H "X-Authy-API-Key: $AUTHY_API_KEY" \ | ||
--data-urlencode user[email]="user@domain.com" \ | ||
--data-urlencode user[cellphone]="317-338-9302" \ | ||
--data-urlencode user[country_code]="54" | ||
``` | ||
|
||
The response is something like this: | ||
|
||
```json | ||
{"message":"User created successfully.","user":{"id":xxxxxxxx},"success":true} | ||
``` | ||
|
||
Save the user id somewhere and add a reference to the matching SFTPGo account. | ||
|
||
After this step you can use the Authy app installed on your phone to generate TOTP codes. | ||
|
||
Now you can verify the token using an HTTP GET request: | ||
|
||
```bash | ||
export TOKEN=<TOTP you read from Authy app> | ||
export AUTHY_ID=<user id> | ||
curl -i "https://api.authy.com/protected/json/verify/${TOKEN}/${AUTHY_ID}" \ | ||
-H "X-Authy-API-Key: $AUTHY_API_KEY" | ||
``` | ||
|
||
So inside your hook you need to check: | ||
|
||
- the HTTP response code for the verify request, it must be `200` | ||
- the JSON reponse body, it must contains the key `success` with the value `true` (as string) | ||
|
||
If these conditions are met the token is valid and you allow the user to login. | ||
|
||
We provide two examples: | ||
|
||
- [Keyboard interactive authentication](./keyint/README.md) for 2FA using password + Authy one time token. | ||
- [External authentication](./extauth/README.md) using Authy one time tokens as passwords. | ||
|
||
Please note that these are sample programs not intended for production use, you should write your own hook based on them and you should prefer HTTP based hooks if performance is a concern. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# Authy external authentication | ||
|
||
This example shows how to use Authy TOTP token as password for SFTPGo users. Please read the [sample code](./main.go), it should be self explanatory. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
module github.com/drakkan/sftpgo/authy/extauth | ||
|
||
go 1.15 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
package main | ||
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
"io/ioutil" | ||
"log" | ||
"net/http" | ||
"os" | ||
"path/filepath" | ||
"time" | ||
) | ||
|
||
type userMapping struct { | ||
SFTPGoUsername string | ||
AuthyID int64 | ||
AuthyAPIKey string | ||
} | ||
|
||
// we assume that the SFTPGo already exists, we only check the one time token. | ||
// If you need to create the SFTPGo user more fields are needed here | ||
type minimalSFTPGoUser struct { | ||
Status int `json:"status,omitempty"` | ||
Username string `json:"username"` | ||
HomeDir string `json:"home_dir,omitempty"` | ||
Permissions map[string][]string `json:"permissions"` | ||
} | ||
|
||
var ( | ||
mapping []userMapping | ||
) | ||
|
||
func init() { | ||
// this is for demo only, you probably want to get this mapping dynamically, for example using a database query | ||
mapping = append(mapping, userMapping{ | ||
SFTPGoUsername: "<SFTPGo username>", | ||
AuthyID: 1234567, | ||
AuthyAPIKey: "<your api key>", | ||
}) | ||
} | ||
|
||
func printResponse(username string) { | ||
u := minimalSFTPGoUser{ | ||
Username: username, | ||
Status: 1, | ||
HomeDir: filepath.Join(os.TempDir(), username), | ||
} | ||
u.Permissions = make(map[string][]string) | ||
u.Permissions["/"] = []string{"*"} | ||
resp, _ := json.Marshal(u) | ||
fmt.Printf("%v\n", string(resp)) | ||
if len(username) > 0 { | ||
os.Exit(0) | ||
} else { | ||
os.Exit(1) | ||
} | ||
} | ||
|
||
func main() { | ||
// get credentials from env vars | ||
username := os.Getenv("SFTPGO_AUTHD_USERNAME") | ||
password := os.Getenv("SFTPGO_AUTHD_PASSWORD") | ||
if len(password) == 0 { | ||
// login method is not password | ||
printResponse("") | ||
return | ||
} | ||
|
||
for _, m := range mapping { | ||
if m.SFTPGoUsername == username { | ||
// mapping found we can now verify the token | ||
url := fmt.Sprintf("https://api.authy.com/protected/json/verify/%v/%v", password, m.AuthyID) | ||
req, err := http.NewRequest(http.MethodGet, url, nil) | ||
if err != nil { | ||
log.Fatal(err) | ||
} | ||
req.Header.Set("X-Authy-API-Key", m.AuthyAPIKey) | ||
httpClient := &http.Client{ | ||
Timeout: 10 * time.Second, | ||
} | ||
resp, err := httpClient.Do(req) | ||
if err != nil { | ||
printResponse("") | ||
} | ||
defer resp.Body.Close() | ||
if resp.StatusCode != http.StatusOK { | ||
// status code 200 is expected | ||
printResponse("") | ||
} | ||
var authyResponse map[string]interface{} | ||
respBody, err := ioutil.ReadAll(resp.Body) | ||
if err != nil { | ||
printResponse("") | ||
} | ||
err = json.Unmarshal(respBody, &authyResponse) | ||
if err != nil { | ||
printResponse("") | ||
} | ||
if authyResponse["success"].(string) == "true" { | ||
printResponse(username) | ||
} | ||
printResponse("") | ||
break | ||
} | ||
} | ||
|
||
// no mapping found | ||
printResponse("") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# Authy keyboard interactive authentication | ||
|
||
This example shows how to authenticate SFTP users using 2FA (password + Authy token). Please read the [sample code](./main.go), it should be self explanatory. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
module github.com/drakkan/sftpgo/authy/keyint | ||
|
||
go 1.15 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
package main | ||
|
||
import ( | ||
"bufio" | ||
"encoding/json" | ||
"fmt" | ||
"io/ioutil" | ||
"net/http" | ||
"os" | ||
"time" | ||
) | ||
|
||
type userMapping struct { | ||
SFTPGoUsername string | ||
AuthyID int64 | ||
AuthyAPIKey string | ||
} | ||
|
||
type keyboardAuthHookResponse struct { | ||
Instruction string `json:"instruction,omitempty"` | ||
Questions []string `json:"questions,omitempty"` | ||
Echos []bool `json:"echos,omitempty"` | ||
AuthResult int `json:"auth_result"` | ||
CheckPwd int `json:"check_password,omitempty"` | ||
} | ||
|
||
var ( | ||
mapping []userMapping | ||
) | ||
|
||
func init() { | ||
// this is for demo only, you probably want to get this mapping dynamically, for example using a database query | ||
mapping = append(mapping, userMapping{ | ||
SFTPGoUsername: "<SFTPGo username>", | ||
AuthyID: 1234567, | ||
AuthyAPIKey: "<your api key>", | ||
}) | ||
} | ||
|
||
func printAuthResponse(result int) { | ||
resp, _ := json.Marshal(keyboardAuthHookResponse{ | ||
AuthResult: result, | ||
}) | ||
fmt.Printf("%v\n", string(resp)) | ||
if result == 1 { | ||
os.Exit(0) | ||
} else { | ||
os.Exit(1) | ||
} | ||
} | ||
|
||
func main() { | ||
// get credentials from env vars | ||
username := os.Getenv("SFTPGO_AUTHD_USERNAME") | ||
var userMap userMapping | ||
for _, m := range mapping { | ||
if m.SFTPGoUsername == username { | ||
userMap = m | ||
break | ||
} | ||
} | ||
|
||
if userMap.SFTPGoUsername != username { | ||
// no mapping found | ||
os.Exit(1) | ||
} | ||
|
||
checkPwdQuestion := keyboardAuthHookResponse{ | ||
Instruction: "This is a sample keyboard authentication program that ask for your password + Authy token", | ||
Questions: []string{"Your password: "}, | ||
Echos: []bool{false}, | ||
CheckPwd: 1, | ||
AuthResult: 0, | ||
} | ||
|
||
q, _ := json.Marshal(checkPwdQuestion) | ||
fmt.Printf("%v\n", string(q)) | ||
|
||
// in a real world app you probably want to use a read timeout | ||
scanner := bufio.NewScanner(os.Stdin) | ||
scanner.Scan() | ||
if scanner.Err() != nil { | ||
printAuthResponse(-1) | ||
} | ||
response := scanner.Text() | ||
if response != "OK" { | ||
printAuthResponse(-1) | ||
} | ||
|
||
checkTokenQuestion := keyboardAuthHookResponse{ | ||
Instruction: "", | ||
Questions: []string{"Authy token: "}, | ||
Echos: []bool{false}, | ||
CheckPwd: 0, | ||
AuthResult: 0, | ||
} | ||
|
||
q, _ = json.Marshal(checkTokenQuestion) | ||
fmt.Printf("%v\n", string(q)) | ||
scanner.Scan() | ||
if scanner.Err() != nil { | ||
printAuthResponse(-1) | ||
} | ||
authyToken := scanner.Text() | ||
|
||
url := fmt.Sprintf("https://api.authy.com/protected/json/verify/%v/%v", authyToken, userMap.AuthyID) | ||
req, err := http.NewRequest(http.MethodGet, url, nil) | ||
if err != nil { | ||
printAuthResponse(-1) | ||
} | ||
req.Header.Set("X-Authy-API-Key", userMap.AuthyAPIKey) | ||
httpClient := &http.Client{ | ||
Timeout: 10 * time.Second, | ||
} | ||
resp, err := httpClient.Do(req) | ||
if err != nil { | ||
printAuthResponse(-1) | ||
} | ||
defer resp.Body.Close() | ||
if resp.StatusCode != http.StatusOK { | ||
// status code 200 is expected | ||
printAuthResponse(-1) | ||
} | ||
var authyResponse map[string]interface{} | ||
respBody, err := ioutil.ReadAll(resp.Body) | ||
if err != nil { | ||
printAuthResponse(-1) | ||
} | ||
err = json.Unmarshal(respBody, &authyResponse) | ||
if err != nil { | ||
printAuthResponse(-1) | ||
} | ||
if authyResponse["success"].(string) == "true" { | ||
printAuthResponse(1) | ||
} | ||
printAuthResponse(-1) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
[Unit] | ||
Description=SFTPGo SFTP Server | ||
Description=SFTPGo Server | ||
After=network.target | ||
|
||
[Service] | ||
|