diff --git a/helper/mfa/totp/mfa.go b/helper/mfa/totp/mfa.go new file mode 100755 index 000000000000..b18327193b3f --- /dev/null +++ b/helper/mfa/totp/mfa.go @@ -0,0 +1,88 @@ +// Package mfa provides wrappers to add multi-factor authentication +// to any auth backend. +// +// To add MFA to a backend, replace its login path with the +// paths returned by MFAPaths and add the additional root +// paths returned by MFARootPaths. The backend provides +// the username to the MFA wrapper in Auth.Metadata['username']. +// +// To add an additional MFA type, create a subpackage that +// implements [Type]Paths, [Type]RootPaths, and [Type]Handler +// functions and add them to MFAPaths, MFARootPaths, and +// handlers respectively. +package mfa + +import ( + "github.com/hashicorp/vault/helper/mfa/duo" + "github.com/hashicorp/vault/helper/mfa/totp" + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +// MFAPaths returns paths to wrap the original login path and configure MFA. +// When adding MFA to a backend, these paths should be included instead of +// the login path in Backend.Paths. +func MFAPaths(originalBackend *framework.Backend, loginPath *framework.Path) []*framework.Path { + var b backend + b.Backend = originalBackend + return append(/*duo.DuoPaths(),*/vaultTotp.TotpPaths(), pathMFAConfig(&b), wrapLoginPath(&b, loginPath)) +} + +// MFARootPaths returns path strings used to configure MFA. When adding MFA +// to a backend, these paths should be included in +// Backend.PathsSpecial.Root. +func MFARootPaths() []string { + return append(/*duo.DuoRootPaths(),*/vaultTotp.TotpRootPaths() , "mfa_config") +} + +// HandlerFunc is the callback called to handle MFA for a login request. +type HandlerFunc func (*logical.Request, *framework.FieldData, *logical.Response) (*logical.Response, error) + +// handlers maps each supported MFA type to its handler. +var handlers = map[string]HandlerFunc{ + "duo": duo.DuoHandler, + "totp": vaultTotp.TotpHandler, +} + +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 MFA 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 + } + + // perform multi-factor authentication if type supported + handler, ok := handlers[mfa_config.Type] + if ok { + return handler(req, d, resp) + } else { + return resp, err + } + } +} diff --git a/helper/mfa/totp/totpmfa.go b/helper/mfa/totp/totpmfa.go new file mode 100755 index 000000000000..57614a5cba35 --- /dev/null +++ b/helper/mfa/totp/totpmfa.go @@ -0,0 +1,73 @@ +// Package duo provides a totp MFA handler. +// This handler is registered as the "totp" type in +// mfa_config. +package vaultTotp + +import ( + "fmt" + + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" + totp "github.com/ptollot/totp" +) + +// TotpPaths returns path functions to configure Duo. +func TotpPaths() []*framework.Path { + return []*framework.Path{ + pathTotpConfig(), + } +} + +// DuoRootPaths returns the paths that are used to configure Duo. +func TotpRootPaths() []string { + return []string { + "totp/config", + } +} + +// TotpHandler use the totp library to authenticate a user +// login request. If successful, the original response from the login +// backend is returned. +func TotpHandler(req *logical.Request, d *framework.FieldData, resp *logical.Response) ( + *logical.Response, error) { + + username, ok := resp.Auth.Metadata["username"] + if !ok { + return logical.ErrorResponse("Could not read username for MFA"), nil + } + + var request *totpAuthRequest = &totpAuthRequest{} + request.successResp = resp + request.username = username + request.method = d.Get("method").(string) + request.passcode = d.Get("passcode").(string) + + return totpHandler(request) +} + +type totpAuthRequest struct { + successResp *logical.Response + username string + method string + passcode string +} + +func totpHandler(request *totpAuthRequest) ( + *logical.Response, error) { + + userPresent := totp.TotpReference.UserVerify(request.username) + if userPresent == false { + return nil, fmt.Errorf("unknown user") + } + + correctPasscode, err := totp.TotpReference.Verify(request.username, request.passcode) + if err != nil { + return nil, err + } + + if correctPasscode { + return request.successResp, nil + } + + return nil, fmt.Errorf("passcode not correct") +} diff --git a/helper/mfa/totp/totpmfa_config.go b/helper/mfa/totp/totpmfa_config.go new file mode 100755 index 000000000000..1350bfedd4af --- /dev/null +++ b/helper/mfa/totp/totpmfa_config.go @@ -0,0 +1,79 @@ +package vaultTotp + +import ( + strings "strings" + + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" + totp "github.com/ptollot/totp" +) + +func pathTotpConfig() *framework.Path { + return &framework.Path{ + + Pattern: `totp/config`, + Fields: map[string]*framework.FieldSchema{ + "username": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "User identifier", + }, + "seed": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "string associated with the user for the creation of totp codes", + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.WriteOperation: pathTotpConfigWrite, + logical.ReadOperation: pathTotpConfigRead, + logical.DeleteOperation: pathTotpConfigDelete, + }, + + HelpSynopsis: pathTotpConfigHelpSyn, + HelpDescription: pathTotpConfigHelpDesc, + } +} + +func pathTotpConfigWrite( + req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + username := d.Get("username").(string) + seed := d.Get("seed").(string) + totp.TotpReference.Register(username, seed) + return &logical.Response{ + Data: map[string]interface{}{ + "username": username, + }, + }, nil +} + +func pathTotpConfigDelete( + req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + username := d.Get("username").(string) + totp.TotpReference.Delete(username) + return &logical.Response{ + Data: map[string]interface{}{ + "username": username, + }, + }, nil +} + +func pathTotpConfigRead( + req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + + userNames := totp.TotpReference.GetUsernames() + + return &logical.Response{ + Data: map[string]interface{}{ + "username": strings.Join(userNames, ", "), + }, + }, nil +} + +const pathTotpConfigHelpSyn = ` +Configure totp second factor behavior. +` + +const pathTotpConfigHelpDesc = ` +This endpoint allows you to configure how the original auth backend username maps to +the totp username by providing a template format string. +`