mfa: add MFA wrapper with Duo second factor

This commit is contained in:
Bradley Girardeau 2015-07-27 11:23:34 -07:00
parent 83729a3bd9
commit 5cf78d8ba2
5 changed files with 454 additions and 0 deletions

101
helper/mfa/duo/duo.go Normal file
View 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
}

View 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.
`

View 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
View 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
}
}
}

View 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.
`