| // Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: MPL-2.0 |
| |
| package userpass |
| |
| import ( |
| "context" |
| "crypto/subtle" |
| "fmt" |
| "strings" |
| |
| "github.com/hashicorp/vault/sdk/framework" |
| "github.com/hashicorp/vault/sdk/helper/cidrutil" |
| "github.com/hashicorp/vault/sdk/helper/policyutil" |
| "github.com/hashicorp/vault/sdk/logical" |
| "golang.org/x/crypto/bcrypt" |
| ) |
| |
| func pathLogin(b *backend) *framework.Path { |
| return &framework.Path{ |
| Pattern: "login/" + framework.GenericNameRegex("username"), |
| |
| DisplayAttrs: &framework.DisplayAttributes{ |
| OperationPrefix: operationPrefixUserpass, |
| OperationVerb: "login", |
| }, |
| |
| Fields: map[string]*framework.FieldSchema{ |
| "username": { |
| Type: framework.TypeString, |
| Description: "Username of the user.", |
| }, |
| |
| "password": { |
| Type: framework.TypeString, |
| Description: "Password for this user.", |
| }, |
| }, |
| |
| Callbacks: map[logical.Operation]framework.OperationFunc{ |
| logical.UpdateOperation: b.pathLogin, |
| logical.AliasLookaheadOperation: b.pathLoginAliasLookahead, |
| }, |
| |
| HelpSynopsis: pathLoginSyn, |
| HelpDescription: pathLoginDesc, |
| } |
| } |
| |
| func (b *backend) pathLoginAliasLookahead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { |
| username := d.Get("username").(string) |
| if username == "" { |
| return nil, fmt.Errorf("missing username") |
| } |
| |
| return &logical.Response{ |
| Auth: &logical.Auth{ |
| Alias: &logical.Alias{ |
| Name: username, |
| }, |
| }, |
| }, nil |
| } |
| |
| func (b *backend) pathLogin(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { |
| username := strings.ToLower(d.Get("username").(string)) |
| |
| password := d.Get("password").(string) |
| if password == "" { |
| return nil, fmt.Errorf("missing password") |
| } |
| |
| // Get the user and validate auth |
| user, userError := b.user(ctx, req.Storage, username) |
| |
| var userPassword []byte |
| var legacyPassword bool |
| // If there was an error or it's nil, we fake a password for the bcrypt |
| // check so as not to have a timing leak. Specifics of the underlying |
| // storage still leaks a bit but generally much more in the noise compared |
| // to bcrypt. |
| if user != nil && userError == nil { |
| if user.PasswordHash == nil { |
| userPassword = []byte(user.Password) |
| legacyPassword = true |
| } else { |
| userPassword = user.PasswordHash |
| } |
| } else { |
| // This is still acceptable as bcrypt will still make sure it takes |
| // a long time, it's just nicer to be random if possible |
| userPassword = []byte("dummy") |
| } |
| |
| // Check for a password match. Check for a hash collision for Vault 0.2+, |
| // but handle the older legacy passwords with a constant time comparison. |
| passwordBytes := []byte(password) |
| switch { |
| case !legacyPassword: |
| if err := bcrypt.CompareHashAndPassword(userPassword, passwordBytes); err != nil { |
| // The failed login info of existing users alone are tracked as only |
| // existing user's failed login information is stored in storage for optimization |
| if user == nil || userError != nil { |
| return logical.ErrorResponse("invalid username or password"), nil |
| } |
| return logical.ErrorResponse("invalid username or password"), logical.ErrInvalidCredentials |
| } |
| default: |
| if subtle.ConstantTimeCompare(userPassword, passwordBytes) != 1 { |
| // The failed login info of existing users alone are tracked as only |
| // existing user's failed login information is stored in storage for optimization |
| if user == nil || userError != nil { |
| return logical.ErrorResponse("invalid username or password"), nil |
| } |
| return logical.ErrorResponse("invalid username or password"), logical.ErrInvalidCredentials |
| } |
| |
| } |
| |
| if userError != nil { |
| return nil, userError |
| } |
| if user == nil { |
| return logical.ErrorResponse("invalid username or password"), nil |
| } |
| |
| // Check for a CIDR match. |
| if len(user.TokenBoundCIDRs) > 0 { |
| if req.Connection == nil { |
| b.Logger().Warn("token bound CIDRs found but no connection information available for validation") |
| return nil, logical.ErrPermissionDenied |
| } |
| if !cidrutil.RemoteAddrIsOk(req.Connection.RemoteAddr, user.TokenBoundCIDRs) { |
| return nil, logical.ErrPermissionDenied |
| } |
| } |
| |
| auth := &logical.Auth{ |
| Metadata: map[string]string{ |
| "username": username, |
| }, |
| DisplayName: username, |
| Alias: &logical.Alias{ |
| Name: username, |
| }, |
| } |
| user.PopulateTokenAuth(auth) |
| |
| return &logical.Response{ |
| Auth: auth, |
| }, nil |
| } |
| |
| func (b *backend) pathLoginRenew(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { |
| // Get the user |
| user, err := b.user(ctx, req.Storage, req.Auth.Metadata["username"]) |
| if err != nil { |
| return nil, err |
| } |
| if user == nil { |
| // User no longer exists, do not renew |
| return nil, nil |
| } |
| |
| if !policyutil.EquivalentPolicies(user.TokenPolicies, req.Auth.TokenPolicies) { |
| return nil, fmt.Errorf("policies have changed, not renewing") |
| } |
| |
| resp := &logical.Response{Auth: req.Auth} |
| resp.Auth.Period = user.TokenPeriod |
| resp.Auth.TTL = user.TokenTTL |
| resp.Auth.MaxTTL = user.TokenMaxTTL |
| return resp, nil |
| } |
| |
| const pathLoginSyn = ` |
| Log in with a username and password. |
| ` |
| |
| const pathLoginDesc = ` |
| This endpoint authenticates using a username and password. |
| ` |