Commit e3e62a95 authored by Kurt Jung's avatar Kurt Jung Committed by Matt Holt

basicauth: Ability to customize realm (#1491)

* Support realms with basic authentication

* Add test for default basicauth directive in which realm is not specified

* Correct typo: missing space

* Remove 'path' subdirective
parent 6bc3e753
...@@ -37,6 +37,7 @@ type BasicAuth struct { ...@@ -37,6 +37,7 @@ type BasicAuth struct {
// ServeHTTP implements the httpserver.Handler interface. // ServeHTTP implements the httpserver.Handler interface.
func (a BasicAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { func (a BasicAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
var protected, isAuthenticated bool var protected, isAuthenticated bool
var realm string
for _, rule := range a.Rules { for _, rule := range a.Rules {
for _, res := range rule.Resources { for _, res := range rule.Resources {
...@@ -46,6 +47,7 @@ func (a BasicAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error ...@@ -46,6 +47,7 @@ func (a BasicAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error
// path matches; this endpoint is protected // path matches; this endpoint is protected
protected = true protected = true
realm = rule.Realm
// parse auth header // parse auth header
username, password, ok := r.BasicAuth() username, password, ok := r.BasicAuth()
...@@ -74,7 +76,10 @@ func (a BasicAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error ...@@ -74,7 +76,10 @@ func (a BasicAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error
// browsers show a message that says something like: // browsers show a message that says something like:
// "The website says: <realm>" // "The website says: <realm>"
// which is kinda dumb, but whatever. // which is kinda dumb, but whatever.
w.Header().Set("WWW-Authenticate", "Basic realm=\"Restricted\"") if realm == "" {
realm = "Restricted"
}
w.Header().Set("WWW-Authenticate", "Basic realm=\""+realm+"\"")
return http.StatusUnauthorized, nil return http.StatusUnauthorized, nil
} }
...@@ -89,6 +94,7 @@ type Rule struct { ...@@ -89,6 +94,7 @@ type Rule struct {
Username string Username string
Password func(string) bool Password func(string) bool
Resources []string Resources []string
Realm string // See RFC 1945 and RFC 2617, default: "Restricted"
} }
// PasswordMatcher determines whether a password matches a rule. // PasswordMatcher determines whether a password matches a rule.
......
...@@ -25,10 +25,20 @@ func TestBasicAuth(t *testing.T) { ...@@ -25,10 +25,20 @@ func TestBasicAuth(t *testing.T) {
} }
return http.StatusOK, nil return http.StatusOK, nil
} }
rw := BasicAuth{ rws := []BasicAuth{
Next: httpserver.HandlerFunc(upstreamHandler), {
Rules: []Rule{ Next: httpserver.HandlerFunc(upstreamHandler),
{Username: "okuser", Password: PlainMatcher("okpass"), Resources: []string{"/testing"}}, Rules: []Rule{
{Username: "okuser", Password: PlainMatcher("okpass"),
Resources: []string{"/testing"}, Realm: "Resources"},
},
},
{
Next: httpserver.HandlerFunc(upstreamHandler),
Rules: []Rule{
{Username: "okuser", Password: PlainMatcher("okpass"),
Resources: []string{"/testing"}},
},
}, },
} }
...@@ -51,34 +61,40 @@ func TestBasicAuth(t *testing.T) { ...@@ -51,34 +61,40 @@ func TestBasicAuth(t *testing.T) {
} }
var test testType var test testType
for i, test = range tests { for _, rw := range rws {
req, err := http.NewRequest("GET", test.from, nil) expectRealm := rw.Rules[0].Realm
if err != nil { if expectRealm == "" {
t.Fatalf("Test %d: Could not create HTTP request: %v", i, err) expectRealm = "Restricted" // Default if Realm not specified in rule
} }
req.SetBasicAuth(test.user, test.password) for i, test = range tests {
req, err := http.NewRequest("GET", test.from, nil)
if err != nil {
t.Fatalf("Test %d: Could not create HTTP request: %v", i, err)
}
req.SetBasicAuth(test.user, test.password)
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
result, err := rw.ServeHTTP(rec, req) result, err := rw.ServeHTTP(rec, req)
if err != nil { if err != nil {
t.Fatalf("Test %d: Could not ServeHTTP: %v", i, err) t.Fatalf("Test %d: Could not ServeHTTP: %v", i, err)
} }
if result != test.result { if result != test.result {
t.Errorf("Test %d: Expected status code %d but was %d", t.Errorf("Test %d: Expected status code %d but was %d",
i, test.result, result) i, test.result, result)
} }
if test.result == http.StatusUnauthorized { if test.result == http.StatusUnauthorized {
headers := rec.Header() headers := rec.Header()
if val, ok := headers["Www-Authenticate"]; ok { if val, ok := headers["Www-Authenticate"]; ok {
if got, want := val[0], "Basic realm=\"Restricted\""; got != want { if got, want := val[0], "Basic realm=\""+expectRealm+"\""; got != want {
t.Errorf("Test %d: Www-Authenticate header should be '%s', got: '%s'", i, want, got) t.Errorf("Test %d: Www-Authenticate header should be '%s', got: '%s'", i, want, got)
}
} else {
t.Errorf("Test %d: response should have a 'Www-Authenticate' header", i)
} }
} else { } else {
t.Errorf("Test %d: response should have a 'Www-Authenticate' header", i) if got, want := req.Header.Get("Authorization"), ""; got != want {
} t.Errorf("Test %d: Expected Authorization header to be stripped from request after successful authentication, but is: %s", i, got)
} else { }
if got, want := req.Header.Get("Authorization"), ""; got != want {
t.Errorf("Test %d: Expected Authorization header to be stripped from request after successful authentication, but is: %s", i, got)
} }
} }
} }
......
...@@ -51,13 +51,6 @@ func basicAuthParse(c *caddy.Controller) ([]Rule, error) { ...@@ -51,13 +51,6 @@ func basicAuthParse(c *caddy.Controller) ([]Rule, error) {
if rule.Password, err = passwordMatcher(rule.Username, args[1], cfg.Root); err != nil { if rule.Password, err = passwordMatcher(rule.Username, args[1], cfg.Root); err != nil {
return rules, c.Errf("Get password matcher from %s: %v", c.Val(), err) return rules, c.Errf("Get password matcher from %s: %v", c.Val(), err)
} }
for c.NextBlock() {
rule.Resources = append(rule.Resources, c.Val())
if c.NextArg() {
return rules, c.Errf("Expecting only one resource per line (extra '%s')", c.Val())
}
}
case 3: case 3:
rule.Resources = append(rule.Resources, args[0]) rule.Resources = append(rule.Resources, args[0])
rule.Username = args[1] rule.Username = args[1]
...@@ -68,6 +61,29 @@ func basicAuthParse(c *caddy.Controller) ([]Rule, error) { ...@@ -68,6 +61,29 @@ func basicAuthParse(c *caddy.Controller) ([]Rule, error) {
return rules, c.ArgErr() return rules, c.ArgErr()
} }
// If nested block is present, process it here
for c.NextBlock() {
val := c.Val()
args = c.RemainingArgs()
switch len(args) {
case 0:
// Assume single argument is path resource
rule.Resources = append(rule.Resources, val)
case 1:
if val == "realm" {
if rule.Realm == "" {
rule.Realm = strings.Replace(args[0], `"`, `\"`, -1)
} else {
return rules, c.Errf("\"realm\" subdirective can only be specified once")
}
} else {
return rules, c.Errf("expecting \"realm\", got \"%s\"", val)
}
default:
return rules, c.ArgErr()
}
}
rules = append(rules, rule) rules = append(rules, rule)
} }
......
...@@ -64,12 +64,39 @@ md5:$apr1$l42y8rex$pOA2VJ0x/0TwaFeAF9nX61` ...@@ -64,12 +64,39 @@ md5:$apr1$l42y8rex$pOA2VJ0x/0TwaFeAF9nX61`
}`, false, "pwd", []Rule{ }`, false, "pwd", []Rule{
{Username: "user"}, {Username: "user"},
}}, }},
{`basicauth /resource1 user pwd {
}`, false, "pwd", []Rule{
{Username: "user", Resources: []string{"/resource1"}},
}},
{`basicauth /resource1 user pwd {
realm Resources
}`, false, "pwd", []Rule{
{Username: "user", Resources: []string{"/resource1"}, Realm: "Resources"},
}},
{`basicauth user pwd { {`basicauth user pwd {
/resource1 /resource1
/resource2 /resource2
}`, false, "pwd", []Rule{ }`, false, "pwd", []Rule{
{Username: "user", Resources: []string{"/resource1", "/resource2"}}, {Username: "user", Resources: []string{"/resource1", "/resource2"}},
}}, }},
{`basicauth user pwd {
/resource1
/resource2
realm "Secure resources"
}`, false, "pwd", []Rule{
{Username: "user", Resources: []string{"/resource1", "/resource2"}, Realm: "Secure resources"},
}},
{`basicauth user pwd {
/resource1
realm "Secure resources"
realm Extra
/resource2
}`, true, "pwd", []Rule{}},
{`basicauth user pwd {
/resource1
foo "Resources"
/resource2
}`, true, "pwd", []Rule{}},
{`basicauth /resource user pwd`, false, "pwd", []Rule{ {`basicauth /resource user pwd`, false, "pwd", []Rule{
{Username: "user", Resources: []string{"/resource"}}, {Username: "user", Resources: []string{"/resource"}},
}}, }},
...@@ -109,6 +136,11 @@ md5:$apr1$l42y8rex$pOA2VJ0x/0TwaFeAF9nX61` ...@@ -109,6 +136,11 @@ md5:$apr1$l42y8rex$pOA2VJ0x/0TwaFeAF9nX61`
i, j, expectedRule.Username, actualRule.Username) i, j, expectedRule.Username, actualRule.Username)
} }
if actualRule.Realm != expectedRule.Realm {
t.Errorf("Test %d, rule %d: Expected realm '%s', got '%s'",
i, j, expectedRule.Realm, actualRule.Realm)
}
if strings.Contains(test.input, "htpasswd=") && skipHtpassword { if strings.Contains(test.input, "htpasswd=") && skipHtpassword {
continue continue
} }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment