Skip to content

Commit

Permalink
Merge pull request #99 from Mygod/v2multiauth
Browse files Browse the repository at this point in the history
Fix multiauth for caddy2
  • Loading branch information
gaby authored Nov 5, 2023
2 parents 9de7188 + 5b0898c commit 31f01c9
Show file tree
Hide file tree
Showing 3 changed files with 49 additions and 34 deletions.
18 changes: 12 additions & 6 deletions caddyfile.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package forwardproxy

import (
"encoding/base64"
"log"
"strconv"
"strings"
Expand All @@ -21,6 +22,14 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
return &fp, err
}

// EncodeAuthCredentials base64-encode credentials
func EncodeAuthCredentials(user, pass string) (result []byte) {
raw := []byte(user + ":" + pass)
result = make([]byte, base64.StdEncoding.EncodedLen(len(raw)))
base64.StdEncoding.Encode(result, raw)
return
}

// UnmarshalCaddyfile unmarshals Caddyfile tokens into h.
func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
if !d.Next() {
Expand All @@ -45,13 +54,10 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
if strings.Contains(args[0], ":") {
return d.Err("character ':' in usernames is not allowed")
}
// TODO: Support multiple basicauths.
// TODO: Actually, just try to use Caddy 2's existing basicauth module.
if h.BasicauthUser != "" || h.BasicauthPass != "" {
return d.Err("Multi-user basicauth is not supported")
if h.AuthCredentials == nil {
h.AuthCredentials = [][]byte{}
}
h.BasicauthUser = args[0]
h.BasicauthPass = args[1]
h.AuthCredentials = append(h.AuthCredentials, EncodeAuthCredentials(args[0], args[1]))
case "hosts":
if len(args) == 0 {
return d.ArgErr()
Expand Down
22 changes: 9 additions & 13 deletions common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,21 +201,19 @@ func TestMain(m *testing.M) {
root: "./test/forwardproxy",
tls: true,
proxyHandler: &Handler{
PACPath: defaultPACPath,
ACL: []ACLRule{{Subjects: []string{"all"}, Allow: true}},
BasicauthUser: "test",
BasicauthPass: "pass",
PACPath: defaultPACPath,
ACL: []ACLRule{{Subjects: []string{"all"}, Allow: true}},
AuthCredentials: [][]byte{EncodeAuthCredentials("test", "pass")},
},
}

caddyHTTPForwardProxyAuth = caddyTestServer{
addr: "127.0.69.73:6973",
root: "./test/forwardproxy",
proxyHandler: &Handler{
PACPath: defaultPACPath,
ACL: []ACLRule{{Subjects: []string{"all"}, Allow: true}},
BasicauthUser: "test",
BasicauthPass: "pass",
PACPath: defaultPACPath,
ACL: []ACLRule{{Subjects: []string{"all"}, Allow: true}},
AuthCredentials: [][]byte{EncodeAuthCredentials("test", "pass")},
},
}

Expand All @@ -227,8 +225,7 @@ func TestMain(m *testing.M) {
PACPath: "/superhiddenfile.pac",
ACL: []ACLRule{{Subjects: []string{"all"}, Allow: true}},
ProbeResistance: &ProbeResistance{Domain: "test.localhost"},
BasicauthUser: "test",
BasicauthPass: "pass",
AuthCredentials: [][]byte{EncodeAuthCredentials("test", "pass")},
},
httpRedirPort: "8880",
}
Expand All @@ -255,9 +252,8 @@ func TestMain(m *testing.M) {
root: "./test/upstreamingproxy",
tls: true,
proxyHandler: &Handler{
Upstream: "https://test:[email protected]:4891",
BasicauthUser: "upstreamtest",
BasicauthPass: "upstreampass",
Upstream: "https://test:[email protected]:4891",
AuthCredentials: [][]byte{EncodeAuthCredentials("upstreamtest", "upstreampass")},
},
}

Expand Down
43 changes: 28 additions & 15 deletions forwardproxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (
"strings"
"sync"
"time"
"unicode/utf8"

caddy "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
Expand Down Expand Up @@ -91,10 +92,7 @@ type Handler struct {
aclRules []aclRule

// TODO: temporary/deprecated - we should try to reuse existing authentication modules instead!
BasicauthUser string `json:"auth_user_deprecated,omitempty"`
BasicauthPass string `json:"auth_pass_deprecated,omitempty"`
authRequired bool
authCredentials [][]byte // slice with base64-encoded credentials
AuthCredentials [][]byte `json:"auth_credentials,omitempty"` // slice with base64-encoded credentials
}

// CaddyModule returns the Caddy module information.
Expand All @@ -120,14 +118,6 @@ func (h *Handler) Provision(ctx caddy.Context) error {
TLSHandshakeTimeout: 10 * time.Second,
}

// TODO: temporary, in an effort to get the tests to pass
if h.BasicauthUser != "" && h.BasicauthPass != "" {
basicAuthBuf := make([]byte, base64.StdEncoding.EncodedLen(len(h.BasicauthUser)+1+len(h.BasicauthPass)))
base64.StdEncoding.Encode(basicAuthBuf, []byte(h.BasicauthUser+":"+h.BasicauthPass))
h.authRequired = true
h.authCredentials = [][]byte{basicAuthBuf}
}

// access control lists
for _, rule := range h.ACL {
for _, subj := range rule.Subjects {
Expand Down Expand Up @@ -155,7 +145,7 @@ func (h *Handler) Provision(ctx caddy.Context) error {
h.aclRules = append(h.aclRules, &aclAllRule{allow: true})

if h.ProbeResistance != nil {
if !h.authRequired {
if h.AuthCredentials == nil {
return fmt.Errorf("probe resistance requires authentication")
}
if len(h.ProbeResistance.Domain) > 0 {
Expand Down Expand Up @@ -237,7 +227,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
}

var authErr error
if h.authRequired {
if h.AuthCredentials != nil {
authErr = h.checkCredentials(r)
}
if h.ProbeResistance != nil && len(h.ProbeResistance.Domain) > 0 && reqHost == h.ProbeResistance.Domain {
Expand Down Expand Up @@ -421,14 +411,37 @@ func (h Handler) checkCredentials(r *http.Request) error {
if strings.ToLower(pa[0]) != "basic" {
return errors.New("auth type is not supported")
}
for _, creds := range h.authCredentials {
for _, creds := range h.AuthCredentials {
if subtle.ConstantTimeCompare(creds, []byte(pa[1])) == 1 {
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
buf := make([]byte, base64.StdEncoding.DecodedLen(len(creds)))
_, _ = base64.StdEncoding.Decode(buf, creds) // should not err ever since we are decoding a known good input
cred := string(buf)
repl.Set("http.auth.user.id", cred[:strings.IndexByte(cred, ':')])
// Please do not consider this to be timing-attack-safe code. Simple equality is almost
// mindlessly substituted with constant time algo and there ARE known issues with this code,
// e.g. size of smallest credentials is guessable. TODO: protect from all the attacks! Hash?
return nil
}
}
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
buf := make([]byte, base64.StdEncoding.DecodedLen(len([]byte(pa[1]))))
n, err := base64.StdEncoding.Decode(buf, []byte(pa[1]))
if err != nil {
repl.Set("http.auth.user.id", "invalidbase64:"+err.Error())
return err
}
if utf8.Valid(buf[:n]) {
cred := string(buf[:n])
i := strings.IndexByte(cred, ':')
if i >= 0 {
repl.Set("http.auth.user.id", "invalid:"+cred[:i])
} else {
repl.Set("http.auth.user.id", "invalidformat:"+cred)
}
} else {
repl.Set("http.auth.user.id", "invalid::")
}
return errors.New("invalid credentials")
}

Expand Down

0 comments on commit 31f01c9

Please sign in to comment.