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 }