deprecating Legacy MFA (#14869)

* deprecating Legacy MFA

* removing legacy MFA doc json entry

* CL

* changing the link to legacy MFA in CL

* removing legacy MFA stuff from credentials' cli
This commit is contained in:
Hamid Ghaf 2022-04-19 21:19:34 -04:00 committed by GitHub
parent 457f28240e
commit 6ff678000e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 14 additions and 994 deletions

View File

@ -5,7 +5,6 @@ import (
"github.com/google/go-github/github"
cleanhttp "github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/vault/helper/mfa"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/logical"
"golang.org/x/oauth2"
@ -40,15 +39,12 @@ func Backend() *backend {
Help: backendHelp,
PathsSpecial: &logical.Paths{
Root: mfa.MFARootPaths(),
Unauthenticated: []string{
"login",
},
},
Paths: append([]*framework.Path{
pathConfig(&b),
}, append(allPaths, mfa.MFAPaths(b.Backend, pathLogin(&b))...)...),
Paths: append([]*framework.Path{pathConfig(&b), pathLogin(&b)}, allPaths...),
AuthRenew: b.pathLoginRenew,
BackendType: logical.TypeCredential,
}

View File

@ -6,7 +6,6 @@ import (
"strings"
"github.com/hashicorp/go-secure-stdlib/strutil"
"github.com/hashicorp/vault/helper/mfa"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/ldaputil"
"github.com/hashicorp/vault/sdk/logical"
@ -28,8 +27,6 @@ func Backend() *backend {
Help: backendHelp,
PathsSpecial: &logical.Paths{
Root: mfa.MFARootPaths(),
Unauthenticated: []string{
"login/*",
},
@ -39,15 +36,14 @@ func Backend() *backend {
},
},
Paths: append([]*framework.Path{
Paths: []*framework.Path{
pathConfig(&b),
pathGroups(&b),
pathGroupsList(&b),
pathUsers(&b),
pathUsersList(&b),
pathLogin(&b),
},
mfa.MFAPaths(b.Backend, pathLogin(&b))...,
),
AuthRenew: b.pathLoginRenew,
BackendType: logical.TypeCredential,

View File

@ -39,15 +39,6 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, erro
"password": password,
}
mfa_method, ok := m["method"]
if ok {
data["method"] = mfa_method
}
mfa_passcode, ok := m["passcode"]
if ok {
data["passcode"] = mfa_passcode
}
path := fmt.Sprintf("auth/%s/login/%s", mount, username)
secret, err := c.Logical().Write(path, data)
if err != nil {
@ -67,11 +58,6 @@ Usage: vault login -method=ldap [CONFIG K=V...]
The LDAP auth method allows users to authenticate using LDAP or
Active Directory.
If MFA is enabled, a "method" and/or "passcode" may be required depending on
the MFA method. To check which MFA is in use, run:
$ vault read auth/<mount>/mfa_config
Authenticate as "sally":
$ vault login -method=ldap username=sally
@ -83,12 +69,6 @@ Usage: vault login -method=ldap [CONFIG K=V...]
Configuration:
method=<string>
MFA method.
passcode=<string>
MFA OTP/passcode.
password=<string>
LDAP password to use for authentication. If not provided, the CLI will
prompt for this on stdin.

View File

@ -7,7 +7,6 @@ import (
"time"
"github.com/hashicorp/go-secure-stdlib/strutil"
"github.com/hashicorp/vault/helper/mfa"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/cidrutil"
"github.com/hashicorp/vault/sdk/logical"
@ -33,8 +32,6 @@ func Backend() *backend {
Help: backendHelp,
PathsSpecial: &logical.Paths{
Root: mfa.MFARootPaths(),
Unauthenticated: []string{
"login/*",
},
@ -43,15 +40,14 @@ func Backend() *backend {
},
},
Paths: append([]*framework.Path{
Paths: []*framework.Path{
pathConfig(&b),
pathUsers(&b),
pathGroups(&b),
pathUsersList(&b),
pathGroupsList(&b),
pathLogin(&b),
},
mfa.MFAPaths(b.Backend, pathLogin(&b))...,
),
AuthRenew: b.pathLoginRenew,
BackendType: logical.TypeCredential,

View File

@ -48,16 +48,6 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, erro
data["provider"] = provider
}
// Legacy MFA support
mfa_method, ok := m["method"]
if ok {
data["method"] = mfa_method
}
mfa_passcode, ok := m["passcode"]
if ok {
data["passcode"] = mfa_passcode
}
path := fmt.Sprintf("auth/%s/login/%s", mount, username)
secret, err := c.Logical().Write(path, data)
if err != nil {

View File

@ -3,7 +3,6 @@ package radius
import (
"context"
"github.com/hashicorp/vault/helper/mfa"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/logical"
)
@ -22,8 +21,6 @@ func Backend() *backend {
Help: backendHelp,
PathsSpecial: &logical.Paths{
Root: mfa.MFARootPaths(),
Unauthenticated: []string{
"login",
"login/*",
@ -34,13 +31,12 @@ func Backend() *backend {
},
},
Paths: append([]*framework.Path{
Paths: []*framework.Path{
pathConfig(&b),
pathUsers(&b),
pathUsersList(&b),
pathLogin(&b),
},
mfa.MFAPaths(b.Backend, pathLogin(&b))...,
),
AuthRenew: b.pathLoginRenew,
BackendType: logical.TypeCredential,

View File

@ -3,7 +3,6 @@ package userpass
import (
"context"
"github.com/hashicorp/vault/helper/mfa"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/logical"
)
@ -22,21 +21,18 @@ func Backend() *backend {
Help: backendHelp,
PathsSpecial: &logical.Paths{
Root: mfa.MFARootPaths(),
Unauthenticated: []string{
"login/*",
},
},
Paths: append([]*framework.Path{
Paths: []*framework.Path{
pathUsers(&b),
pathUsersList(&b),
pathUserPolicies(&b),
pathUserPassword(&b),
pathLogin(&b),
},
mfa.MFAPaths(b.Backend, pathLogin(&b))...,
),
AuthRenew: b.pathLoginRenew,
BackendType: logical.TypeCredential,

View File

@ -19,8 +19,6 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, erro
Username string `mapstructure:"username"`
Password string `mapstructure:"password"`
Mount string `mapstructure:"mount"`
Method string `mapstructure:"method"`
Passcode string `mapstructure:"passcode"`
}
if err := mapstructure.WeakDecode(m, &data); err != nil {
return nil, err
@ -45,12 +43,6 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, erro
options := map[string]interface{}{
"password": data.Password,
}
if data.Method != "" {
options["method"] = data.Method
}
if data.Passcode != "" {
options["passcode"] = data.Passcode
}
path := fmt.Sprintf("auth/%s/login/%s", data.Mount, data.Username)
secret, err := c.Logical().Write(path, options)
@ -71,11 +63,6 @@ Usage: vault login -method=userpass [CONFIG K=V...]
The userpass auth method allows users to authenticate using Vault's
internal user database.
If MFA is enabled, a "method" and/or "passcode" may be required depending on
the MFA method. To check which MFA is in use, run:
$ vault read auth/<mount>/mfa_config
Authenticate as "sally":
$ vault login -method=userpass username=sally
@ -87,12 +74,6 @@ Usage: vault login -method=userpass [CONFIG K=V...]
Configuration:
method=<string>
MFA method.
passcode=<string>
MFA OTP/passcode.
password=<string>
Password to use for authentication. If not provided, the CLI will prompt
for this on stdin.

4
changelog/14869.txt Normal file
View File

@ -0,0 +1,4 @@
```release-note:change
auth: Remove support for legacy MFA
(https://www.vaultproject.io/docs/v1.10.x/auth/mfa)
```

View File

@ -1,149 +0,0 @@
// Package duo provides a Duo MFA handler to authenticate users
// with Duo. This handler is registered as the "duo" type in
// mfa_config.
package duo
import (
"context"
"fmt"
"net/url"
"github.com/duosecurity/duo_api_golang/authapi"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/logical"
)
// DuoPaths returns path functions to configure Duo.
func DuoPaths() []*framework.Path {
return []*framework.Path{
pathDuoConfig(),
pathDuoAccess(),
}
}
// DuoRootPaths returns the paths that are used to configure Duo.
func DuoRootPaths() []string {
return []string{
"duo/access",
"duo/config",
}
}
// DuoHandler interacts with the Duo Auth API to authenticate a user
// login request. If successful, the original response from the login
// backend is returned.
func DuoHandler(ctx context.Context, req *logical.Request, d *framework.FieldData, resp *logical.Response) (
*logical.Response, error) {
duoConfig, err := GetDuoConfig(ctx, req)
if err != nil || duoConfig == nil {
return logical.ErrorResponse("Could not load Duo configuration"), nil
}
duoAuthClient, err := GetDuoAuthClient(ctx, req, duoConfig)
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
}
var request *duoAuthRequest = &duoAuthRequest{}
request.successResp = resp
request.username = username
request.method = d.Get("method").(string)
request.passcode = d.Get("passcode").(string)
request.ipAddr = req.Connection.RemoteAddr
return duoHandler(duoConfig, duoAuthClient, request)
}
type duoAuthRequest struct {
successResp *logical.Response
username string
method string
passcode string
ipAddr string
}
func duoHandler(duoConfig *DuoConfig, duoAuthClient AuthClient, request *duoAuthRequest) (
*logical.Response, error,
) {
duoUser := fmt.Sprintf(duoConfig.UsernameFormat, request.username)
preauth, err := duoAuthClient.Preauth(
authapi.PreauthUsername(duoUser),
authapi.PreauthIpAddr(request.ipAddr),
)
if err != nil || preauth == nil {
return logical.ErrorResponse("Could not call Duo preauth"), nil
}
if preauth.StatResult.Stat != "OK" {
errorMsg := "Could not look up Duo user information"
if preauth.StatResult.Message != nil {
errorMsg = errorMsg + ": " + *preauth.StatResult.Message
}
if preauth.StatResult.Message_Detail != nil {
errorMsg = errorMsg + " (" + *preauth.StatResult.Message_Detail + ")"
}
return logical.ErrorResponse(errorMsg), nil
}
switch preauth.Response.Result {
case "allow":
return request.successResp, err
case "deny":
return logical.ErrorResponse(preauth.Response.Status_Msg), nil
case "enroll":
return logical.ErrorResponse(fmt.Sprintf("%s (%s)",
preauth.Response.Status_Msg,
preauth.Response.Enroll_Portal_Url)), nil
case "auth":
break
default:
return logical.ErrorResponse(fmt.Sprintf("Invalid Duo preauth response: %s",
preauth.Response.Result)), nil
}
options := []func(*url.Values){authapi.AuthUsername(duoUser)}
if request.method == "" {
request.method = "auto"
}
if request.method == "auto" || request.method == "push" {
if duoConfig.PushInfo != "" {
options = append(options, authapi.AuthPushinfo(duoConfig.PushInfo))
}
}
if request.passcode != "" {
request.method = "passcode"
options = append(options, authapi.AuthPasscode(request.passcode))
} else {
options = append(options, authapi.AuthDevice("auto"))
}
result, err := duoAuthClient.Auth(request.method, options...)
if err != nil || result == nil {
return logical.ErrorResponse("Could not call Duo auth"), nil
}
if result.StatResult.Stat != "OK" {
errorMsg := "Could not authenticate Duo user"
if result.StatResult.Message != nil {
errorMsg = errorMsg + ": " + *result.StatResult.Message
}
if result.StatResult.Message_Detail != nil {
errorMsg = errorMsg + " (" + *result.StatResult.Message_Detail + ")"
}
return logical.ErrorResponse(errorMsg), nil
}
if result.Response.Result != "allow" {
return logical.ErrorResponse(result.Response.Status_Msg), nil
}
return request.successResp, nil
}

View File

@ -1,123 +0,0 @@
package duo
import (
"net/url"
"strings"
"testing"
"github.com/duosecurity/duo_api_golang/authapi"
"github.com/hashicorp/vault/sdk/helper/jsonutil"
"github.com/hashicorp/vault/sdk/logical"
)
type MockClientData struct {
PreauthData *authapi.PreauthResult
PreauthError error
AuthData *authapi.AuthResult
AuthError error
}
type MockAuthClient struct {
MockData *MockClientData
}
func (c *MockAuthClient) Preauth(options ...func(*url.Values)) (*authapi.PreauthResult, error) {
return c.MockData.PreauthData, c.MockData.PreauthError
}
func (c *MockAuthClient) Auth(factor string, options ...func(*url.Values)) (*authapi.AuthResult, error) {
return c.MockData.AuthData, c.MockData.AuthError
}
func MockGetDuoAuthClient(data *MockClientData) func(*logical.Request, *DuoConfig) (AuthClient, error) {
return func(*logical.Request, *DuoConfig) (AuthClient, error) {
return getDuoAuthClient(data), nil
}
}
func getDuoAuthClient(data *MockClientData) AuthClient {
var c MockAuthClient
// set default response to be successful
preauthSuccessJSON := `
{
"Stat": "OK",
"Response": {
"Result": "auth",
"Status_Msg": "Needs authentication",
"Devices": []
}
}`
if data.PreauthData == nil {
data.PreauthData = &authapi.PreauthResult{}
jsonutil.DecodeJSON([]byte(preauthSuccessJSON), data.PreauthData)
}
authSuccessJSON := `
{
"Stat": "OK",
"Response": {
"Result": "allow"
}
}`
if data.AuthData == nil {
data.AuthData = &authapi.AuthResult{}
jsonutil.DecodeJSON([]byte(authSuccessJSON), data.AuthData)
}
c.MockData = data
return &c
}
func TestDuoHandlerSuccess(t *testing.T) {
successResp := &logical.Response{
Auth: &logical.Auth{},
}
duoConfig := &DuoConfig{
UsernameFormat: "%s",
}
duoAuthClient := getDuoAuthClient(&MockClientData{})
resp, err := duoHandler(duoConfig, duoAuthClient, &duoAuthRequest{
successResp: successResp,
username: "",
})
if err != nil {
t.Fatalf(err.Error())
}
if resp != successResp {
t.Fatalf("Testing Duo authentication gave incorrect response (expected success, got: %v)", resp)
}
}
func TestDuoHandlerReject(t *testing.T) {
AuthData := &authapi.AuthResult{}
authRejectJSON := `
{
"Stat": "OK",
"Response": {
"Result": "deny",
"Status_Msg": "Invalid auth"
}
}`
jsonutil.DecodeJSON([]byte(authRejectJSON), AuthData)
successResp := &logical.Response{
Auth: &logical.Auth{},
}
expectedError := AuthData.Response.Status_Msg
duoConfig := &DuoConfig{
UsernameFormat: "%s",
}
duoAuthClient := getDuoAuthClient(&MockClientData{
AuthData: AuthData,
})
resp, err := duoHandler(duoConfig, duoAuthClient, &duoAuthRequest{
successResp: successResp,
username: "user",
})
if err != nil {
t.Fatalf(err.Error())
}
error, ok := resp.Data["error"].(string)
if !ok || !strings.Contains(error, expectedError) {
t.Fatalf("Testing Duo authentication gave incorrect response (expected deny, got: %v)", error)
}
}

View File

@ -1,119 +0,0 @@
package duo
import (
"context"
"fmt"
"net/url"
duoapi "github.com/duosecurity/duo_api_golang"
"github.com/duosecurity/duo_api_golang/authapi"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/logical"
)
type AuthClient interface {
Preauth(options ...func(*url.Values)) (*authapi.PreauthResult, error)
Auth(factor string, options ...func(*url.Values)) (*authapi.AuthResult, error)
}
func pathDuoAccess() *framework.Path {
return &framework.Path{
Pattern: `duo/access`,
Fields: map[string]*framework.FieldSchema{
"skey": {
Type: framework.TypeString,
Description: "Duo secret key",
},
"ikey": {
Type: framework.TypeString,
Description: "Duo integration key",
},
"host": {
Type: framework.TypeString,
Description: "Duo api host",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.UpdateOperation: pathDuoAccessWrite,
},
HelpSynopsis: pathDuoAccessHelpSyn,
HelpDescription: pathDuoAccessHelpDesc,
}
}
func GetDuoAuthClient(ctx context.Context, req *logical.Request, config *DuoConfig) (AuthClient, error) {
entry, err := req.Storage.Get(ctx, "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
}
duoClient := duoapi.NewDuoApi(
access.IKey,
access.SKey,
access.Host,
config.UserAgent,
)
duoAuthClient := authapi.NewAuthApi(*duoClient)
check, err := duoAuthClient.Check()
if err != nil {
return nil, err
}
if check == nil {
return nil, fmt.Errorf("could not connect to Duo; got nil result back from API check call")
}
var msg, detail string
if check.StatResult.Message != nil {
msg = *check.StatResult.Message
}
if check.StatResult.Message_Detail != nil {
detail = *check.StatResult.Message_Detail
}
if check.StatResult.Stat != "OK" {
return nil, fmt.Errorf("could not connect to Duo: %q (%q)", msg, detail)
}
return duoAuthClient, nil
}
func pathDuoAccessWrite(ctx context.Context, 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(ctx, 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

@ -1,110 +0,0 @@
package duo
import (
"context"
"errors"
"strings"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/logical"
)
func pathDuoConfig() *framework.Path {
return &framework.Path{
Pattern: `duo/config`,
Fields: map[string]*framework.FieldSchema{
"user_agent": {
Type: framework.TypeString,
Description: "User agent to connect to Duo (default \"\")",
},
"username_format": {
Type: framework.TypeString,
Description: "Format string given auth method username as argument to create Duo username (default '%s')",
},
"push_info": {
Type: framework.TypeString,
Description: "A string of URL-encoded key/value pairs that provides additional context about the authentication attempt in the Duo Mobile app",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.UpdateOperation: pathDuoConfigWrite,
logical.ReadOperation: pathDuoConfigRead,
},
HelpSynopsis: pathDuoConfigHelpSyn,
HelpDescription: pathDuoConfigHelpDesc,
}
}
func GetDuoConfig(ctx context.Context, req *logical.Request) (*DuoConfig, error) {
var result DuoConfig
// all config parameters are optional, so path need not exist
entry, err := req.Storage.Get(ctx, "duo/config")
if err == nil && entry != nil {
if err := entry.DecodeJSON(&result); err != nil {
return nil, err
}
}
if result.UsernameFormat == "" {
result.UsernameFormat = "%s"
}
return &result, nil
}
func pathDuoConfigWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
username_format := d.Get("username_format").(string)
if username_format == "" {
username_format = "%s"
}
if !strings.Contains(username_format, "%s") {
return nil, errors.New("username_format must include username ('%s')")
}
entry, err := logical.StorageEntryJSON("duo/config", DuoConfig{
UsernameFormat: username_format,
UserAgent: d.Get("user_agent").(string),
PushInfo: d.Get("push_info").(string),
})
if err != nil {
return nil, err
}
if err := req.Storage.Put(ctx, entry); err != nil {
return nil, err
}
return nil, nil
}
func pathDuoConfigRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
config, err := GetDuoConfig(ctx, req)
if err != nil {
return nil, err
}
if config == nil {
return nil, nil
}
return &logical.Response{
Data: map[string]interface{}{
"username_format": config.UsernameFormat,
"user_agent": config.UserAgent,
"push_info": config.PushInfo,
},
}, nil
}
type DuoConfig struct {
UsernameFormat string `json:"username_format"`
UserAgent string `json:"user_agent"`
PushInfo string `json:"push_info"`
}
const pathDuoConfigHelpSyn = `
Configure Duo second factor behavior.
`
const pathDuoConfigHelpDesc = `
This endpoint allows you to configure how the original auth method username maps to
the Duo username by providing a template format string.
`

View File

@ -1,88 +0,0 @@
// Package mfa provides wrappers to add multi-factor authentication
// to any auth method.
//
// 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 (
"context"
"github.com/hashicorp/vault/helper/mfa/duo"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/logical"
)
// 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(), 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(), "mfa_config")
}
// HandlerFunc is the callback called to handle MFA for a login request.
type HandlerFunc func(context.Context, *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,
}
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.UpdateOperation]
loginPath.Callbacks[logical.UpdateOperation] = b.wrapLoginHandler(loginHandler)
return loginPath
}
func (b *backend) wrapLoginHandler(loginHandler framework.OperationFunc) framework.OperationFunc {
return func(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
// login with original login function first
resp, err := loginHandler(ctx, req, d)
if err != nil || resp.Auth == nil {
return resp, err
}
// check if multi-factor enabled
mfa_config, err := b.MFAConfig(ctx, 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(ctx, req, d, resp)
} else {
return resp, err
}
}
}

View File

@ -1,129 +0,0 @@
package mfa
import (
"context"
"testing"
logicaltest "github.com/hashicorp/vault/helper/testhelpers/logical"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/logical"
)
// MakeTestBackend creates a simple MFA enabled backend.
// Login (before MFA) always succeeds with policy "foo".
// An MFA "test" type is added to mfa.handlers that succeeds
// if MFA method is "accept", otherwise it rejects.
func MakeTestBackend() *framework.Backend {
handlers["test"] = testMFAHandler
b := &framework.Backend{
Help: "",
PathsSpecial: &logical.Paths{
Root: MFARootPaths(),
Unauthenticated: []string{
"login",
},
},
Paths: MFAPaths(nil, testPathLogin()),
}
return b
}
func testPathLogin() *framework.Path {
return &framework.Path{
Pattern: `login`,
Fields: map[string]*framework.FieldSchema{
"username": {
Type: framework.TypeString,
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.UpdateOperation: testPathLoginHandler,
},
}
}
func testPathLoginHandler(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
username := d.Get("username").(string)
return &logical.Response{
Auth: &logical.Auth{
Policies: []string{"foo"},
Metadata: map[string]string{
"username": username,
},
},
}, nil
}
func testMFAHandler(ctx context.Context, req *logical.Request, d *framework.FieldData, resp *logical.Response) (
*logical.Response, error) {
if d.Get("method").(string) != "accept" {
return logical.ErrorResponse("Deny access"), nil
} else {
return resp, nil
}
}
func TestMFALogin(t *testing.T) {
b := MakeTestBackend()
logicaltest.Test(t, logicaltest.TestCase{
AcceptanceTest: true,
LogicalBackend: b,
Steps: []logicaltest.TestStep{
testAccStepEnableMFA(t),
testAccStepLogin(t, "user"),
},
})
}
func TestMFALoginDenied(t *testing.T) {
b := MakeTestBackend()
logicaltest.Test(t, logicaltest.TestCase{
AcceptanceTest: true,
LogicalBackend: b,
Steps: []logicaltest.TestStep{
testAccStepEnableMFA(t),
testAccStepLoginDenied(t, "user"),
},
})
}
func testAccStepEnableMFA(t *testing.T) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "mfa_config",
Data: map[string]interface{}{
"type": "test",
},
}
}
func testAccStepLogin(t *testing.T, username string) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "login",
Data: map[string]interface{}{
"method": "accept",
"username": username,
},
Unauthenticated: true,
Check: logicaltest.TestCheckAuth([]string{"foo"}),
}
}
func testAccStepLoginDenied(t *testing.T, username string) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "login",
Data: map[string]interface{}{
"method": "deny",
"username": username,
},
Unauthenticated: true,
Check: logicaltest.TestCheckError(),
}
}

View File

@ -1,87 +0,0 @@
package mfa
import (
"context"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/logical"
)
func pathMFAConfig(b *backend) *framework.Path {
return &framework.Path{
Pattern: `mfa_config`,
Fields: map[string]*framework.FieldSchema{
"type": {
Type: framework.TypeString,
Description: "Enables MFA with given backend (available: duo)",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.UpdateOperation: b.pathMFAConfigWrite,
logical.ReadOperation: b.pathMFAConfigRead,
},
HelpSynopsis: pathMFAConfigHelpSyn,
HelpDescription: pathMFAConfigHelpDesc,
}
}
func (b *backend) MFAConfig(ctx context.Context, req *logical.Request) (*MFAConfig, error) {
entry, err := req.Storage.Get(ctx, "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(ctx context.Context, 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(ctx, entry); err != nil {
return nil, err
}
return nil, nil
}
func (b *backend) pathMFAConfigRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
config, err := b.MFAConfig(ctx, 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.
`

View File

@ -1,106 +0,0 @@
---
layout: docs
page_title: Multi-Factor Authentication (MFA) - Auth Methods
description: |-
Multi-factor authentication (MFA) is supported for several authentication
methods.
---
# Multi-Factor Authentication
~> **NOTE**: This page describes the legacy MFA system available in the OSS
edition of Vault. This system is not supported by HashiCorp. Vault Enterprise
contains a fully-supported MFA system that is significantly more complete and
flexible and which can be used throughout Vault's API. See the [Vault
Enterprise MFA](/docs/enterprise/mfa) page for more information.
Several auth methods support multi-factor authentication (MFA). Once
enabled for a method, users are required to provide additional verification,
like a one-time passcode, before being authenticated.
Currently, the "ldap", "okta", "radius", and "userpass" backends support MFA.
## Authentication
When authenticating, users still provide the same information as before, in
addition to MFA verification. Usually this is a passcode, but in other cases,
like a Duo Push notification, no additional information is needed.
### Via the CLI
```tedt
$ vault login -method=userpass \
username=my-username \
password=test \
passcode=111111
```
```shell-session
$ vault login -method=userpass \
username=my-username \
password=test \
method=push
```
### Via the API
The endpoint for the login is the same as for the original method. Additional
MFA information should be sent in the POST body encoded as JSON.
```shell-session
$ curl \
--request POST \
--data '{"password": "test", "passcode": "111111"}' \
http://127.0.0.1:8200/v1/auth/userpass/login/my-username
```
The response is the same as for the original method.
## Configuration
To enable MFA for a supported method, the MFA type must be set in `mfa_config`.
For example:
```shell-session
$ vault write auth/userpass/mfa_config type=duo
```
This enables the Duo MFA type, which is currently the only MFA type supported.
The username used for MFA is the same as the login username, unless the method
or MFA type provide options to behave differently (see Duo configuration below).
### Duo
The Duo MFA type is configured through two paths: `duo/config` and `duo/access`.
`duo/access` contains connection information for the Duo Auth API. To configure:
```shell-session
$ vault write auth/[mount]/duo/access \
host=[host] \
ikey=[integration key] \
skey=[secret key]
```
`duo/config` is an optional path that contains general configuration information
for Duo authentication. To configure:
```shell-session
$ vault write auth/[mount]/duo/config \
user_agent="" \
username_format="%s"
```
- `user_agent` is the user agent to use when connecting to Duo.
- `username_format` controls how the username used to login is transformed
before authenticating with Duo. This field is a format string that is passed
the original username as its first argument and outputs the new username. For
example "%s@example.com" would append "@example.com" to the provided username
before connecting to Duo.
- `push_info` is a string of URL-encoded key/value pairs that provides
additional context about the authentication attempt in the Duo Mobile
application.
More information can be found through the CLI `path-help` command.

View File

@ -23,7 +23,7 @@ This announcement page is maintained and updated periodically to communicate imp
| End of Support: Etcd V2 API (OSS) | v1.9 | N/A | v1.10 | The Etcd v2 has been deprecated with the release of Etcd v3.5, and will be decomissioned by Etcd v3.6. Etcd v2 API has been removed in Vaut 1.10. Users of Etcd storage backend must migrate Vault storage to an Etcd V3 cluster prior to upgrading to Vault 1.10. All storage migrations should be backed up prior to migration. | [Etcd Storage Backend](/docs/configuration/storage/etcd) |
| End of Support: Licenses in storage (ENT) | v1.8 | v1.10 | v1.11 | Migrate to [Autoloading](/docs/enterprise/license/autoloading) by v1.11. | [Vault License](/docs/enterprise/license) [System Backend](https://www.vaultproject.io/api-docs/system/license) [FAQ](/docs/enterprise/license/faq) |
| Feature Removal: Mount Filters (ENT) | v1.3 | v1.10 | v1.11 | Use the alternative feature: [Path Filters](https://www.vaultproject.io/api-docs/system/replication/replication-performance#create-paths-filter) | [API Deprecation Notice](https://www.vaultproject.io/api-docs/system/replication/replication-performance#create-mounts-filter-deprecated) [Filter Mount Replication Deprecation Notice](/docs/upgrading/upgrade-to-1.3.0#filtered-mount-replication-deprecation) |
| Feature Removal: Legacy MFA (OSS) | v1.0 | N/A | v1.11 | Based on your use case, use the Policy-based Enterprise MFA or Login MFA supported in Vault OSS as of v1.10 | [Multi-Factor Authentication](/docs/auth/mfa) |
| Feature Removal: Legacy MFA (OSS) | v1.0 | N/A | v1.11 | Based on your use case, use the Policy-based Enterprise MFA or Login MFA supported in Vault OSS as of v1.10 | [Multi-Factor Authentication](https://www.vaultproject.io/docs/v1.10.x/auth/mfa) |
| Feature Removal: Standalone DB Engines (OSS) | v0.8 | N/A | v1.11 | Use the alternative DB secrets engine feature | [DB secrets engine](/docs/secrets/databases) |
| Feature Removal: AppID (OSS) | v0.6 | N/A | v1.11 | Use the alternative feature: [AppRole auth method](https://www.vaultproject.io/docs/auth/approle) | [AppID Auth Method Deprecation Notice](/docs/auth/app-id) |
| End of Support: AAD Graph on Azure Secrets Engine | v1.10 | 1.11 | v1.12 | Microsoft will end its support of the [AAD Graph API on June 30, 2022](https://docs.microsoft.com/en-us/graph/migrate-azure-ad-graph-overview). Support for Microsoft Graph API was introduced in Vault 1.9. If your Vault deployment is on a prior release, you may use the Azure Secrets Engine as an external plugin while you plan to upgrade. | [AAD (Azure Active Directory](https://vault-git-post-1-10-doc-changes-hashicorp.vercel.app/docs/secrets/azure#aad-azure-active-directory) |

View File

@ -1256,10 +1256,6 @@
{
"title": "App ID <sup>DEPRECATED</sup>",
"path": "auth/app-id"
},
{
"title": "MFA <sup>LEGACY / UNSUPPORTED</sup>",
"path": "auth/mfa"
}
]
},