mirror of
https://github.com/caddyserver/caddy.git
synced 2025-01-21 01:45:45 +03:00
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
This commit is contained in:
parent
6bc3e7536e
commit
e3e62a952d
4 changed files with 106 additions and 36 deletions
|
@ -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
|
||||||
|
|
||||||
// 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
|
||||||
// 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 {
|
||||||
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) {
|
||||||
}
|
}
|
||||||
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) {
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
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) {
|
||||||
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`
|
||||||
}`, 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`
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue