github.com/avenga/couper@v1.12.2/accesscontrol/basic_auth.go (about)

     1  package accesscontrol
     2  
     3  import (
     4  	"bufio"
     5  	"context"
     6  	"crypto/subtle"
     7  	"fmt"
     8  	"net/http"
     9  	"os"
    10  	"strconv"
    11  	"strings"
    12  
    13  	"github.com/avenga/couper/config/request"
    14  	"github.com/avenga/couper/errors"
    15  )
    16  
    17  var _ AccessControl = &BasicAuth{}
    18  
    19  // BasicAuth represents an AC-BasicAuth object
    20  type BasicAuth struct {
    21  	htFile htData
    22  	name   string
    23  	user   string
    24  	pass   string
    25  }
    26  
    27  // NewBasicAuth creates a new AC-BasicAuth object
    28  func NewBasicAuth(name, user, pass, file string) (*BasicAuth, error) {
    29  	ba := &BasicAuth{
    30  		htFile: make(htData),
    31  		name:   name,
    32  		user:   user,
    33  		pass:   pass,
    34  	}
    35  
    36  	if file == "" {
    37  		return ba, nil
    38  	}
    39  
    40  	fp, err := os.Open(file)
    41  	if err != nil {
    42  		return nil, err
    43  	}
    44  	defer fp.Close()
    45  
    46  	scanner := bufio.NewScanner(fp)
    47  	var lineNr int
    48  	for scanner.Scan() {
    49  		lineNr++
    50  		line := strings.TrimSpace(scanner.Text())
    51  		if len(line) == 0 || line[0] == '#' {
    52  			continue
    53  		}
    54  
    55  		if len(line) > 255 {
    56  			return nil, fmt.Errorf("parse error: line length exceeded: 255")
    57  		}
    58  
    59  		up := strings.SplitN(line, ":", 2)
    60  		if len(up) != 2 {
    61  			return nil, fmt.Errorf("parse error: invalid line: " + strconv.Itoa(lineNr))
    62  		}
    63  
    64  		username, password := up[0], up[1]
    65  
    66  		if _, ok := ba.htFile[username]; ok {
    67  			return nil, fmt.Errorf("multiple user: " + username)
    68  		}
    69  
    70  		switch pwdType := getPwdType(password); pwdType {
    71  		case pwdTypeApr1:
    72  			fallthrough
    73  		case pwdTypeMD5:
    74  			prefix := pwdPrefixApr1
    75  			if pwdType == pwdTypeMD5 {
    76  				prefix = pwdPrefixMD5
    77  			}
    78  
    79  			parts := strings.Split(strings.TrimPrefix(password, prefix), "$")
    80  			if len(parts) != 2 {
    81  				return nil, fmt.Errorf("parse error: malformed password for user: " + username)
    82  			}
    83  
    84  			ba.htFile[username] = pwd{
    85  				pwdOrig:   []byte(password),
    86  				pwdPrefix: prefix,
    87  				pwdSalt:   parts[0],
    88  				pwdType:   pwdType,
    89  			}
    90  		case pwdTypeBcrypt:
    91  			ba.htFile[username] = pwd{
    92  				pwdOrig: []byte(password),
    93  				pwdType: pwdType,
    94  			}
    95  		default:
    96  			return nil, fmt.Errorf("parse error: algorithm not supported")
    97  		}
    98  	}
    99  
   100  	err = scanner.Err()
   101  	return ba, err
   102  }
   103  
   104  // Validate implements the AccessControl interface
   105  func (ba *BasicAuth) Validate(req *http.Request) error {
   106  	if ba == nil {
   107  		return errors.Configuration
   108  	}
   109  
   110  	user, pass, ok := req.BasicAuth()
   111  	if !ok { // false is unspecific, determine if credentials are set
   112  		const prefix = "Basic "
   113  		if val := req.Header.Get("Authorization"); val == "" || !strings.HasPrefix(val, prefix) {
   114  			return errors.BasicAuthCredentialsMissing.Message("credentials required")
   115  		}
   116  		return errors.BasicAuth.Message("reading authorization failed")
   117  	}
   118  
   119  	if ba.user == user {
   120  		if ba.pass != "" {
   121  			if subtle.ConstantTimeCompare([]byte(ba.pass), []byte(pass)) == 1 {
   122  				return ba.withUsername(req, user)
   123  			}
   124  			return errors.BasicAuth.Message("credential mismatch")
   125  		}
   126  
   127  		if len(ba.htFile) == 0 {
   128  			return errors.BasicAuth.Message("no password configured")
   129  		}
   130  	}
   131  
   132  	if len(ba.htFile) > 0 {
   133  		if validateAccessData(user, pass, ba.htFile) {
   134  			return ba.withUsername(req, user)
   135  		}
   136  		return errors.BasicAuth.Message("file: credential mismatch")
   137  	}
   138  
   139  	return errors.BasicAuth.Message("credential mismatch")
   140  }
   141  
   142  func (ba *BasicAuth) withUsername(req *http.Request, user string) error {
   143  	u := make(map[string]interface{})
   144  	u["user"] = user
   145  
   146  	ctx := req.Context()
   147  	acMap, ok := ctx.Value(request.AccessControls).(map[string]interface{})
   148  	if !ok {
   149  		acMap = make(map[string]interface{})
   150  	}
   151  	acMap[ba.name] = u
   152  
   153  	ctx = context.WithValue(ctx, request.AccessControls, acMap)
   154  	*req = *req.WithContext(ctx)
   155  
   156  	return nil
   157  }