mfa: add MFA wrapper with Duo second factor
This commit is contained in:
parent
83729a3bd9
commit
5cf78d8ba2
101
helper/mfa/duo/duo.go
Normal file
101
helper/mfa/duo/duo.go
Normal file
|
@ -0,0 +1,101 @@
|
|||
package duo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/duosecurity/duo_api_golang/authapi"
|
||||
"github.com/hashicorp/vault/logical"
|
||||
"github.com/hashicorp/vault/logical/framework"
|
||||
)
|
||||
|
||||
func DuoPaths() []*framework.Path {
|
||||
return []*framework.Path{
|
||||
pathDuoConfig(),
|
||||
pathDuoAccess(),
|
||||
}
|
||||
}
|
||||
|
||||
func DuoPathsSpecial() []string {
|
||||
return []string {
|
||||
"duo/access",
|
||||
"duo/config",
|
||||
}
|
||||
}
|
||||
|
||||
func DuoHandler(req *logical.Request, d *framework.FieldData, resp *logical.Response) (
|
||||
*logical.Response, error) {
|
||||
duo_config, err := GetDuoConfig(req)
|
||||
if err != nil || duo_config == nil {
|
||||
return logical.ErrorResponse("Could not load Duo configuration"), nil
|
||||
}
|
||||
|
||||
duo_auth_client, err := GetDuoAuthClient(req, duo_config)
|
||||
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
|
||||
}
|
||||
|
||||
duo_user := fmt.Sprintf(duo_config.UsernameFormat, username)
|
||||
|
||||
preauth, err := duo_auth_client.Preauth(
|
||||
authapi.PreauthUsername(duo_user),
|
||||
authapi.PreauthIpAddr(req.Connection.RemoteAddr),
|
||||
)
|
||||
|
||||
if preauth.StatResult.Stat != "OK" {
|
||||
return logical.ErrorResponse(fmt.Sprintf("Could not look up Duo user information: %s (%s)",
|
||||
*preauth.StatResult.Message,
|
||||
*preauth.StatResult.Message_Detail,
|
||||
)), nil
|
||||
}
|
||||
|
||||
switch preauth.Response.Result {
|
||||
case "allow":
|
||||
return resp, err
|
||||
case "deny":
|
||||
return logical.ErrorResponse(preauth.Response.Status_Msg), nil
|
||||
case "enroll":
|
||||
return logical.ErrorResponse(preauth.Response.Status_Msg), nil
|
||||
case "auth":
|
||||
break
|
||||
}
|
||||
|
||||
options := []func(*url.Values){authapi.AuthUsername(duo_user)}
|
||||
|
||||
method := d.Get("method").(string)
|
||||
if method == "" {
|
||||
method = "auto"
|
||||
}
|
||||
|
||||
passcode := d.Get("passcode").(string)
|
||||
if passcode != "" {
|
||||
method = "passcode"
|
||||
options = append(options, authapi.AuthPasscode(passcode))
|
||||
} else {
|
||||
options = append(options, authapi.AuthDevice("auto"))
|
||||
}
|
||||
|
||||
result, err := duo_auth_client.Auth(method, options...)
|
||||
|
||||
if err != nil {
|
||||
return logical.ErrorResponse("Could not call Duo auth"), nil
|
||||
}
|
||||
|
||||
if result.StatResult.Stat != "OK" {
|
||||
return logical.ErrorResponse(fmt.Sprintf("Could not authenticate Duo user: %s (%s)",
|
||||
*preauth.StatResult.Message,
|
||||
*preauth.StatResult.Message_Detail,
|
||||
)), nil
|
||||
}
|
||||
|
||||
if result.Response.Result != "allow" {
|
||||
return logical.ErrorResponse(result.Response.Status_Msg), nil
|
||||
}
|
||||
|
||||
return resp, err
|
||||
}
|
103
helper/mfa/duo/path_duo_access.go
Normal file
103
helper/mfa/duo/path_duo_access.go
Normal file
|
@ -0,0 +1,103 @@
|
|||
package duo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/duosecurity/duo_api_golang"
|
||||
"github.com/duosecurity/duo_api_golang/authapi"
|
||||
"github.com/hashicorp/vault/logical"
|
||||
"github.com/hashicorp/vault/logical/framework"
|
||||
)
|
||||
|
||||
func pathDuoAccess() *framework.Path {
|
||||
return &framework.Path{
|
||||
Pattern: `duo/access`,
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
"skey": &framework.FieldSchema{
|
||||
Type: framework.TypeString,
|
||||
Description: "Duo secret key",
|
||||
},
|
||||
"ikey": &framework.FieldSchema{
|
||||
Type: framework.TypeString,
|
||||
Description: "Duo integration key",
|
||||
},
|
||||
"host": &framework.FieldSchema{
|
||||
Type: framework.TypeString,
|
||||
Description: "Duo api host",
|
||||
},
|
||||
},
|
||||
|
||||
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||
logical.WriteOperation: pathDuoAccessWrite,
|
||||
},
|
||||
|
||||
HelpSynopsis: pathDuoAccessHelpSyn,
|
||||
HelpDescription: pathDuoAccessHelpDesc,
|
||||
}
|
||||
}
|
||||
|
||||
func GetDuoAuthClient(req *logical.Request, config *DuoConfig) (*authapi.AuthApi, error) {
|
||||
entry, err := req.Storage.Get("duo/access")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if entry == nil {
|
||||
return nil, fmt.Errorf(
|
||||
"Duo access credentials haven't been configured. Please configure\n" +
|
||||
"them at the 'duo/access' endpoint")
|
||||
}
|
||||
var access DuoAccess
|
||||
if err := entry.DecodeJSON(&access); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
duo_client := duoapi.NewDuoApi(
|
||||
access.IKey,
|
||||
access.SKey,
|
||||
access.Host,
|
||||
config.UserAgent,
|
||||
)
|
||||
duo_auth_client := authapi.NewAuthApi(*duo_client)
|
||||
check, err := duo_auth_client.Check()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if check.StatResult.Stat != "OK" {
|
||||
return nil, fmt.Errorf("Could not connect to Duo: %s (%s)", *check.StatResult.Message, *check.StatResult.Message_Detail)
|
||||
}
|
||||
return duo_auth_client, nil
|
||||
}
|
||||
|
||||
func pathDuoAccessWrite(
|
||||
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
entry, err := logical.StorageEntryJSON("duo/access", DuoAccess{
|
||||
SKey: d.Get("skey").(string),
|
||||
IKey: d.Get("ikey").(string),
|
||||
Host: d.Get("host").(string),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := req.Storage.Put(entry); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type DuoAccess struct {
|
||||
SKey string `json:"skey"`
|
||||
IKey string `json:"ikey"`
|
||||
Host string `json:"host"`
|
||||
}
|
||||
|
||||
const pathDuoAccessHelpSyn = `
|
||||
Configure the access keys and host for Duo API connections.
|
||||
`
|
||||
|
||||
const pathDuoAccessHelpDesc = `
|
||||
To authenticate users with Duo, the backend needs to know what host to connect to
|
||||
and must authenticate with an integration key and secret key. This endpoint is used
|
||||
to configure that information.
|
||||
`
|
103
helper/mfa/duo/path_duo_config.go
Normal file
103
helper/mfa/duo/path_duo_config.go
Normal file
|
@ -0,0 +1,103 @@
|
|||
package duo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/vault/logical"
|
||||
"github.com/hashicorp/vault/logical/framework"
|
||||
)
|
||||
|
||||
func pathDuoConfig() *framework.Path {
|
||||
return &framework.Path{
|
||||
Pattern: `duo/config`,
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
"user_agent": &framework.FieldSchema{
|
||||
Type: framework.TypeString,
|
||||
Description: "User agent to connect to Duo (default \"\")",
|
||||
},
|
||||
"username_format": &framework.FieldSchema{
|
||||
Type: framework.TypeString,
|
||||
Description: "Format string given auth backend username as argument to create Duo username (default '%s')",
|
||||
},
|
||||
},
|
||||
|
||||
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||
logical.WriteOperation: pathDuoConfigWrite,
|
||||
logical.ReadOperation: pathDuoConfigRead,
|
||||
},
|
||||
|
||||
HelpSynopsis: pathDuoConfigHelpSyn,
|
||||
HelpDescription: pathDuoConfigHelpDesc,
|
||||
}
|
||||
}
|
||||
|
||||
func GetDuoConfig(req *logical.Request) (*DuoConfig, error) {
|
||||
entry, err := req.Storage.Get("duo/config")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if entry == nil {
|
||||
return nil, nil
|
||||
}
|
||||
var result DuoConfig
|
||||
if err := entry.DecodeJSON(&result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result.UsernameFormat == "" {
|
||||
result.UsernameFormat = "%s"
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func pathDuoConfigWrite(
|
||||
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
username_format := d.Get("username_format").(string)
|
||||
if !strings.Contains(username_format, "%s") {
|
||||
return nil, fmt.Errorf("username_format must include username ('%s')")
|
||||
}
|
||||
entry, err := logical.StorageEntryJSON("duo/config", DuoConfig{
|
||||
UsernameFormat: username_format,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := req.Storage.Put(entry); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func pathDuoConfigRead(
|
||||
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
|
||||
config, err := GetDuoConfig(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if config == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &logical.Response{
|
||||
Data: map[string]interface{}{
|
||||
"username_format": config.UsernameFormat,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
type DuoConfig struct {
|
||||
UsernameFormat string `json:"username_format"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
}
|
||||
|
||||
const pathDuoConfigHelpSyn = `
|
||||
Configure Duo second factor behavior.
|
||||
`
|
||||
|
||||
const pathDuoConfigHelpDesc = `
|
||||
This endpoint allows you to configure how the original auth backend username maps to
|
||||
the Duo username by providing a template format string.
|
||||
`
|
59
helper/mfa/mfa.go
Normal file
59
helper/mfa/mfa.go
Normal file
|
@ -0,0 +1,59 @@
|
|||
package mfa
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/vault/helper/mfa/duo"
|
||||
"github.com/hashicorp/vault/logical"
|
||||
"github.com/hashicorp/vault/logical/framework"
|
||||
)
|
||||
|
||||
func MFAPaths(originalBackend *framework.Backend, loginPath *framework.Path) []*framework.Path {
|
||||
var b backend
|
||||
b.Backend = originalBackend
|
||||
return append(duo.DuoPaths(), pathMFAConfig(&b), wrapLoginPath(&b, loginPath))
|
||||
}
|
||||
|
||||
func MFAPathsSpecial() []string {
|
||||
return append(duo.DuoPathsSpecial(), "mfa_config")
|
||||
}
|
||||
|
||||
type backend struct {
|
||||
*framework.Backend
|
||||
}
|
||||
|
||||
func wrapLoginPath(b *backend, loginPath *framework.Path) *framework.Path {
|
||||
(*loginPath).Fields["passcode"] = &framework.FieldSchema{
|
||||
Type: framework.TypeString,
|
||||
Description: "One time passcode (optional)",
|
||||
}
|
||||
(*loginPath).Fields["method"] = &framework.FieldSchema{
|
||||
Type: framework.TypeString,
|
||||
Description: "Multi-factor auth method to use (optional)",
|
||||
}
|
||||
// wrap write callback to do duo two factor after auth
|
||||
loginHandler := loginPath.Callbacks[logical.WriteOperation]
|
||||
loginPath.Callbacks[logical.WriteOperation] = b.wrapLoginHandler(loginHandler)
|
||||
return loginPath
|
||||
}
|
||||
|
||||
func (b *backend) wrapLoginHandler(loginHandler framework.OperationFunc) framework.OperationFunc {
|
||||
return func (req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
// login with original login function first
|
||||
resp, err := loginHandler(req, d);
|
||||
if err != nil || resp.Auth == nil {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// check if multi-factor enabled
|
||||
mfa_config, err := b.MFAConfig(req)
|
||||
if err != nil || mfa_config == nil {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
switch (mfa_config.Type) {
|
||||
case "duo":
|
||||
return duo.DuoHandler(req, d, resp)
|
||||
default:
|
||||
return resp, err
|
||||
}
|
||||
}
|
||||
}
|
88
helper/mfa/path_mfa_config.go
Normal file
88
helper/mfa/path_mfa_config.go
Normal file
|
@ -0,0 +1,88 @@
|
|||
package mfa
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/vault/logical"
|
||||
"github.com/hashicorp/vault/logical/framework"
|
||||
)
|
||||
|
||||
func pathMFAConfig(b *backend) *framework.Path {
|
||||
return &framework.Path{
|
||||
Pattern: `mfa_config`,
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
"type": &framework.FieldSchema{
|
||||
Type: framework.TypeString,
|
||||
Description: "Enables MFA with given backend (available: duo)",
|
||||
},
|
||||
},
|
||||
|
||||
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||
logical.WriteOperation: b.pathMFAConfigWrite,
|
||||
logical.ReadOperation: b.pathMFAConfigRead,
|
||||
},
|
||||
|
||||
HelpSynopsis: pathMFAConfigHelpSyn,
|
||||
HelpDescription: pathMFAConfigHelpDesc,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *backend) MFAConfig(req *logical.Request) (*MFAConfig, error) {
|
||||
entry, err := req.Storage.Get("mfa_config")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if entry == nil {
|
||||
return nil, nil
|
||||
}
|
||||
var result MFAConfig
|
||||
if err := entry.DecodeJSON(&result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (b *backend) pathMFAConfigWrite(
|
||||
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
entry, err := logical.StorageEntryJSON("mfa_config", MFAConfig{
|
||||
Type: d.Get("type").(string),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := req.Storage.Put(entry); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (b *backend) pathMFAConfigRead(
|
||||
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
|
||||
config, err := b.MFAConfig(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if config == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &logical.Response{
|
||||
Data: map[string]interface{}{
|
||||
"type": config.Type,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
type MFAConfig struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
const pathMFAConfigHelpSyn = `
|
||||
Configure multi factor backend.
|
||||
`
|
||||
|
||||
const pathMFAConfigHelpDesc = `
|
||||
This endpoint allows you to turn on multi-factor authentication with a given backend.
|
||||
Currently only Duo is supported.
|
||||
`
|
Loading…
Reference in a new issue