mfa: add MFA wrapper with Duo second factor
This commit is contained in:
parent
83729a3bd9
commit
5cf78d8ba2
|
@ -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
|
||||||
|
}
|
|
@ -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.
|
||||||
|
`
|
|
@ -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.
|
||||||
|
`
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 New Issue