open-vault/helper/mfa/duo/duo.go

150 lines
4.1 KiB
Go

// Package duo provides a Duo MFA handler to authenticate users
// with Duo. This handler is registered as the "duo" type in
// mfa_config.
package duo
import (
"context"
"fmt"
"net/url"
"github.com/duosecurity/duo_api_golang/authapi"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/logical"
)
// DuoPaths returns path functions to configure Duo.
func DuoPaths() []*framework.Path {
return []*framework.Path{
pathDuoConfig(),
pathDuoAccess(),
}
}
// DuoRootPaths returns the paths that are used to configure Duo.
func DuoRootPaths() []string {
return []string{
"duo/access",
"duo/config",
}
}
// DuoHandler interacts with the Duo Auth API to authenticate a user
// login request. If successful, the original response from the login
// backend is returned.
func DuoHandler(ctx context.Context, req *logical.Request, d *framework.FieldData, resp *logical.Response) (
*logical.Response, error) {
duoConfig, err := GetDuoConfig(ctx, req)
if err != nil || duoConfig == nil {
return logical.ErrorResponse("Could not load Duo configuration"), nil
}
duoAuthClient, err := GetDuoAuthClient(ctx, req, duoConfig)
if err != nil {
return logical.ErrorResponse(err.Error()), nil
}
username, ok := resp.Auth.Metadata["username"]
if !ok {
return logical.ErrorResponse("Could not read username for MFA"), nil
}
var request *duoAuthRequest = &duoAuthRequest{}
request.successResp = resp
request.username = username
request.method = d.Get("method").(string)
request.passcode = d.Get("passcode").(string)
request.ipAddr = req.Connection.RemoteAddr
return duoHandler(duoConfig, duoAuthClient, request)
}
type duoAuthRequest struct {
successResp *logical.Response
username string
method string
passcode string
ipAddr string
}
func duoHandler(duoConfig *DuoConfig, duoAuthClient AuthClient, request *duoAuthRequest) (
*logical.Response, error,
) {
duoUser := fmt.Sprintf(duoConfig.UsernameFormat, request.username)
preauth, err := duoAuthClient.Preauth(
authapi.PreauthUsername(duoUser),
authapi.PreauthIpAddr(request.ipAddr),
)
if err != nil || preauth == nil {
return logical.ErrorResponse("Could not call Duo preauth"), nil
}
if preauth.StatResult.Stat != "OK" {
errorMsg := "Could not look up Duo user information"
if preauth.StatResult.Message != nil {
errorMsg = errorMsg + ": " + *preauth.StatResult.Message
}
if preauth.StatResult.Message_Detail != nil {
errorMsg = errorMsg + " (" + *preauth.StatResult.Message_Detail + ")"
}
return logical.ErrorResponse(errorMsg), nil
}
switch preauth.Response.Result {
case "allow":
return request.successResp, err
case "deny":
return logical.ErrorResponse(preauth.Response.Status_Msg), nil
case "enroll":
return logical.ErrorResponse(fmt.Sprintf("%s (%s)",
preauth.Response.Status_Msg,
preauth.Response.Enroll_Portal_Url)), nil
case "auth":
break
default:
return logical.ErrorResponse(fmt.Sprintf("Invalid Duo preauth response: %s",
preauth.Response.Result)), nil
}
options := []func(*url.Values){authapi.AuthUsername(duoUser)}
if request.method == "" {
request.method = "auto"
}
if request.method == "auto" || request.method == "push" {
if duoConfig.PushInfo != "" {
options = append(options, authapi.AuthPushinfo(duoConfig.PushInfo))
}
}
if request.passcode != "" {
request.method = "passcode"
options = append(options, authapi.AuthPasscode(request.passcode))
} else {
options = append(options, authapi.AuthDevice("auto"))
}
result, err := duoAuthClient.Auth(request.method, options...)
if err != nil || result == nil {
return logical.ErrorResponse("Could not call Duo auth"), nil
}
if result.StatResult.Stat != "OK" {
errorMsg := "Could not authenticate Duo user"
if result.StatResult.Message != nil {
errorMsg = errorMsg + ": " + *result.StatResult.Message
}
if result.StatResult.Message_Detail != nil {
errorMsg = errorMsg + " (" + *result.StatResult.Message_Detail + ")"
}
return logical.ErrorResponse(errorMsg), nil
}
if result.Response.Result != "allow" {
return logical.ErrorResponse(result.Response.Status_Msg), nil
}
return request.successResp, nil
}