1package userpass
2
3import (
4	"context"
5	"crypto/subtle"
6	"fmt"
7	"strings"
8
9	"github.com/hashicorp/vault/sdk/framework"
10	"github.com/hashicorp/vault/sdk/helper/cidrutil"
11	"github.com/hashicorp/vault/sdk/helper/policyutil"
12	"github.com/hashicorp/vault/sdk/logical"
13	"golang.org/x/crypto/bcrypt"
14)
15
16func pathLogin(b *backend) *framework.Path {
17	return &framework.Path{
18		Pattern: "login/" + framework.GenericNameRegex("username"),
19		Fields: map[string]*framework.FieldSchema{
20			"username": &framework.FieldSchema{
21				Type:        framework.TypeString,
22				Description: "Username of the user.",
23			},
24
25			"password": &framework.FieldSchema{
26				Type:        framework.TypeString,
27				Description: "Password for this user.",
28			},
29		},
30
31		Callbacks: map[logical.Operation]framework.OperationFunc{
32			logical.UpdateOperation:         b.pathLogin,
33			logical.AliasLookaheadOperation: b.pathLoginAliasLookahead,
34		},
35
36		HelpSynopsis:    pathLoginSyn,
37		HelpDescription: pathLoginDesc,
38	}
39}
40
41func (b *backend) pathLoginAliasLookahead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
42	username := strings.ToLower(d.Get("username").(string))
43	if username == "" {
44		return nil, fmt.Errorf("missing username")
45	}
46
47	return &logical.Response{
48		Auth: &logical.Auth{
49			Alias: &logical.Alias{
50				Name: username,
51			},
52		},
53	}, nil
54}
55
56func (b *backend) pathLogin(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
57	username := strings.ToLower(d.Get("username").(string))
58
59	password := d.Get("password").(string)
60	if password == "" {
61		return nil, fmt.Errorf("missing password")
62	}
63
64	// Get the user and validate auth
65	user, userError := b.user(ctx, req.Storage, username)
66
67	var userPassword []byte
68	var legacyPassword bool
69	// If there was an error or it's nil, we fake a password for the bcrypt
70	// check so as not to have a timing leak. Specifics of the underlying
71	// storage still leaks a bit but generally much more in the noise compared
72	// to bcrypt.
73	if user != nil && userError == nil {
74		if user.PasswordHash == nil {
75			userPassword = []byte(user.Password)
76			legacyPassword = true
77		} else {
78			userPassword = user.PasswordHash
79		}
80	} else {
81		// This is still acceptable as bcrypt will still make sure it takes
82		// a long time, it's just nicer to be random if possible
83		userPassword = []byte("dummy")
84	}
85
86	// Check for a password match. Check for a hash collision for Vault 0.2+,
87	// but handle the older legacy passwords with a constant time comparison.
88	passwordBytes := []byte(password)
89	if !legacyPassword {
90		if err := bcrypt.CompareHashAndPassword(userPassword, passwordBytes); err != nil {
91			return logical.ErrorResponse("invalid username or password"), nil
92		}
93	} else {
94		if subtle.ConstantTimeCompare(userPassword, passwordBytes) != 1 {
95			return logical.ErrorResponse("invalid username or password"), nil
96		}
97	}
98
99	if userError != nil {
100		return nil, userError
101	}
102	if user == nil {
103		return logical.ErrorResponse("invalid username or password"), nil
104	}
105
106	// Check for a CIDR match.
107	if len(user.TokenBoundCIDRs) > 0 {
108		if req.Connection == nil {
109			b.Logger().Warn("token bound CIDRs found but no connection information available for validation")
110			return nil, logical.ErrPermissionDenied
111		}
112		if !cidrutil.RemoteAddrIsOk(req.Connection.RemoteAddr, user.TokenBoundCIDRs) {
113			return nil, logical.ErrPermissionDenied
114		}
115	}
116
117	auth := &logical.Auth{
118		Metadata: map[string]string{
119			"username": username,
120		},
121		DisplayName: username,
122		Alias: &logical.Alias{
123			Name: username,
124		},
125	}
126	user.PopulateTokenAuth(auth)
127
128	return &logical.Response{
129		Auth: auth,
130	}, nil
131}
132
133func (b *backend) pathLoginRenew(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
134	// Get the user
135	user, err := b.user(ctx, req.Storage, req.Auth.Metadata["username"])
136	if err != nil {
137		return nil, err
138	}
139	if user == nil {
140		// User no longer exists, do not renew
141		return nil, nil
142	}
143
144	if !policyutil.EquivalentPolicies(user.TokenPolicies, req.Auth.TokenPolicies) {
145		return nil, fmt.Errorf("policies have changed, not renewing")
146	}
147
148	resp := &logical.Response{Auth: req.Auth}
149	resp.Auth.Period = user.TokenPeriod
150	resp.Auth.TTL = user.TokenTTL
151	resp.Auth.MaxTTL = user.TokenMaxTTL
152	return resp, nil
153}
154
155const pathLoginSyn = `
156Log in with a username and password.
157`
158
159const pathLoginDesc = `
160This endpoint authenticates using a username and password.
161`
162