external auth http hook: properly serialize the user in the POST body

For historical reasons we send the json serialized user as a string field.
I Initially copied the code used in the script hook where it is appropriate
to convert the JSON user to string.

After some time I have noticed this error, I know that changing it now might
break existing external authentication hooks but we cannot continue with
this mistake, new users are surprised by this behavior, sorry

Fixes #836

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino
2022-05-15 18:26:07 +02:00
parent 18d0bf9dc3
commit 9abd186166
2 changed files with 30 additions and 12 deletions

View File

@@ -3327,7 +3327,9 @@ func ExecutePostLoginHook(user *User, loginMethod, ip, protocol string, err erro
}() }()
} }
func getExternalAuthResponse(username, password, pkey, keyboardInteractive, ip, protocol string, cert *x509.Certificate, userAsJSON []byte) ([]byte, error) { func getExternalAuthResponse(username, password, pkey, keyboardInteractive, ip, protocol string, cert *x509.Certificate,
user User,
) ([]byte, error) {
var tlsCert string var tlsCert string
if cert != nil { if cert != nil {
var err error var err error
@@ -3338,7 +3340,7 @@ func getExternalAuthResponse(username, password, pkey, keyboardInteractive, ip,
} }
if strings.HasPrefix(config.ExternalAuthHook, "http") { if strings.HasPrefix(config.ExternalAuthHook, "http") {
var result []byte var result []byte
authRequest := make(map[string]string) authRequest := make(map[string]interface{})
authRequest["username"] = username authRequest["username"] = username
authRequest["ip"] = ip authRequest["ip"] = ip
authRequest["password"] = password authRequest["password"] = password
@@ -3346,8 +3348,8 @@ func getExternalAuthResponse(username, password, pkey, keyboardInteractive, ip,
authRequest["protocol"] = protocol authRequest["protocol"] = protocol
authRequest["keyboard_interactive"] = keyboardInteractive authRequest["keyboard_interactive"] = keyboardInteractive
authRequest["tls_cert"] = tlsCert authRequest["tls_cert"] = tlsCert
if len(userAsJSON) > 0 { if user.ID > 0 {
authRequest["user"] = string(userAsJSON) authRequest["user"] = user
} }
authRequestAsJSON, err := json.Marshal(authRequest) authRequestAsJSON, err := json.Marshal(authRequest)
if err != nil { if err != nil {
@@ -3367,8 +3369,17 @@ func getExternalAuthResponse(username, password, pkey, keyboardInteractive, ip,
return io.ReadAll(io.LimitReader(resp.Body, maxHookResponseSize)) return io.ReadAll(io.LimitReader(resp.Body, maxHookResponseSize))
} }
var userAsJSON []byte
var err error
if user.ID > 0 {
userAsJSON, err = json.Marshal(user)
if err != nil {
return nil, fmt.Errorf("unable to serialize user as JSON: %w", err)
}
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel() defer cancel()
cmd := exec.CommandContext(ctx, config.ExternalAuthHook) cmd := exec.CommandContext(ctx, config.ExternalAuthHook)
cmd.Env = append(os.Environ(), cmd.Env = append(os.Environ(),
fmt.Sprintf("SFTPGO_AUTHD_USERNAME=%v", username), fmt.Sprintf("SFTPGO_AUTHD_USERNAME=%v", username),
@@ -3396,7 +3407,7 @@ func doExternalAuth(username, password string, pubKey []byte, keyboardInteractiv
) (User, error) { ) (User, error) {
var user User var user User
u, mergedUser, userAsJSON, err := getUserAndJSONForHook(username, nil) u, mergedUser, err := getUserForHook(username, nil)
if err != nil { if err != nil {
return user, err return user, err
} }
@@ -3415,7 +3426,7 @@ func doExternalAuth(username, password string, pubKey []byte, keyboardInteractiv
} }
startTime := time.Now() startTime := time.Now()
out, err := getExternalAuthResponse(username, password, pkey, keyboardInteractive, ip, protocol, tlsCert, userAsJSON) out, err := getExternalAuthResponse(username, password, pkey, keyboardInteractive, ip, protocol, tlsCert, u)
if err != nil { if err != nil {
return user, fmt.Errorf("external auth error for user %#v: %v, elapsed: %v", username, err, time.Since(startTime)) return user, fmt.Errorf("external auth error for user %#v: %v, elapsed: %v", username, err, time.Since(startTime))
} }
@@ -3541,12 +3552,11 @@ func doPluginAuth(username, password string, pubKey []byte, ip, protocol string,
return provider.userExists(user.Username) return provider.userExists(user.Username)
} }
func getUserAndJSONForHook(username string, oidcTokenFields *map[string]interface{}) (User, User, []byte, error) { func getUserForHook(username string, oidcTokenFields *map[string]interface{}) (User, User, error) {
var userAsJSON []byte
u, err := provider.userExists(username) u, err := provider.userExists(username)
if err != nil { if err != nil {
if _, ok := err.(*util.RecordNotFoundError); !ok { if _, ok := err.(*util.RecordNotFoundError); !ok {
return u, u, userAsJSON, err return u, u, err
} }
u = User{ u = User{
BaseUser: sdk.BaseUser{ BaseUser: sdk.BaseUser{
@@ -3558,11 +3568,19 @@ func getUserAndJSONForHook(username string, oidcTokenFields *map[string]interfac
mergedUser := u.getACopy() mergedUser := u.getACopy()
err = mergedUser.LoadAndApplyGroupSettings() err = mergedUser.LoadAndApplyGroupSettings()
if err != nil { if err != nil {
return u, mergedUser, userAsJSON, err return u, mergedUser, err
} }
u.OIDCCustomFields = oidcTokenFields u.OIDCCustomFields = oidcTokenFields
userAsJSON, err = json.Marshal(u) return u, mergedUser, err
}
func getUserAndJSONForHook(username string, oidcTokenFields *map[string]interface{}) (User, User, []byte, error) {
u, mergedUser, err := getUserForHook(username, oidcTokenFields)
if err != nil {
return u, mergedUser, nil, err
}
userAsJSON, err := json.Marshal(u)
if err != nil { if err != nil {
return u, mergedUser, userAsJSON, err return u, mergedUser, userAsJSON, err
} }

View File

@@ -25,7 +25,7 @@ If the hook is an HTTP URL then it will be invoked as HTTP POST. The request bod
- `username` - `username`
- `ip` - `ip`
- `user`, STPGo user serialized as JSON, omitted if the user does not exist within the data provider - `user`, STPGo user, omitted if the user does not exist within the data provider
- `protocol`, possible values are `SSH`, `FTP`, `DAV`, `HTTP` - `protocol`, possible values are `SSH`, `FTP`, `DAV`, `HTTP`
- `password`, not empty for password authentication - `password`, not empty for password authentication
- `public_key`, not empty for public key authentication - `public_key`, not empty for public key authentication