Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added TOTP MFA #812

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions helper/mfa/totp/mfa.go
Original file line number Diff line number Diff line change
@@ -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
}
}
}
73 changes: 73 additions & 0 deletions helper/mfa/totp/totpmfa.go
Original file line number Diff line number Diff line change
@@ -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")
}
79 changes: 79 additions & 0 deletions helper/mfa/totp/totpmfa_config.go
Original file line number Diff line number Diff line change
@@ -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.
`