fup: add TokenAuthMiddleware
This is an example middleware which can be used as an AuthMiddleware for only allowing requests to non-view pages which are accompanied by an auth token via HTTP basic auth.
This commit is contained in:
parent
d96cf3d34d
commit
dbd711ded8
3 changed files with 168 additions and 0 deletions
|
@ -37,6 +37,11 @@ func init() {
|
|||
viper.BindPFlag("serve.cheddar.path", serveCmd.Flags().Lookup("cheddar-path"))
|
||||
serveCmd.Flags().StringVar(&serveCheddarAddr, "cheddar-address", "", "If non-empty, will be used instead of attempting to spawn a copy of cheddar.")
|
||||
viper.BindPFlag("serve.cheddar.address", serveCmd.Flags().Lookup("cheddar-address"))
|
||||
|
||||
serveCmd.Flags().StringVar(&serveAuthToken, "auth-token", "", "If non-empty, this auth token will be required as the Basic Auth password.")
|
||||
viper.BindPFlag("serve.auth.token", serveCmd.Flags().Lookup("auth-token"))
|
||||
serveCmd.Flags().StringVar(&serveAuthRealm, "auth-realm", "fup", "Will be used as the realm for Basic Auth.")
|
||||
viper.BindPFlag("serve.auth.realm", serveCmd.Flags().Lookup("auth-realm"))
|
||||
}
|
||||
|
||||
var (
|
||||
|
@ -46,6 +51,8 @@ var (
|
|||
serveDirectOnly bool
|
||||
serveCheddarPath string
|
||||
serveCheddarAddr string
|
||||
serveAuthToken string
|
||||
serveAuthRealm string
|
||||
|
||||
serveCmd = &cobra.Command{
|
||||
Use: "serve",
|
||||
|
@ -72,6 +79,7 @@ var (
|
|||
StorageURL: bucketURL,
|
||||
RedirectToBlobstore: !serveDirectOnly,
|
||||
Highlighter: highlighter,
|
||||
AuthMiddleware: fuphttp.TokenAuthMiddleware(serveAuthToken, serveAuthRealm),
|
||||
}
|
||||
a, err := fuphttp.New(ctx, cfg)
|
||||
if err != nil {
|
||||
|
|
47
web/fup/fuphttp/auth.go
Normal file
47
web/fup/fuphttp/auth.go
Normal file
|
@ -0,0 +1,47 @@
|
|||
package fuphttp
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func TokenAuthMiddleware(token, realm string) mux.MiddlewareFunc {
|
||||
return func(next http.Handler) http.Handler {
|
||||
if token == "" {
|
||||
return next
|
||||
}
|
||||
tokenBytes := []byte(token)
|
||||
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
if !IsMutate(ctx) {
|
||||
// Allow all access to public pages.
|
||||
next.ServeHTTP(rw, r)
|
||||
return
|
||||
}
|
||||
|
||||
requestAuth := func(s string) {
|
||||
rw.Header().Set("WWW-Authenticate", fmt.Sprintf("Basic realm=%q, charset=\"UTF-8\"", realm))
|
||||
http.Error(rw, s, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
// Check for basic auth, first.
|
||||
_, pw, ok := r.BasicAuth()
|
||||
switch {
|
||||
case !ok:
|
||||
requestAuth("unparsable or no credentials")
|
||||
return
|
||||
case subtle.ConstantTimeCompare([]byte(pw), tokenBytes) != 1:
|
||||
requestAuth("bad credentials")
|
||||
return
|
||||
}
|
||||
|
||||
// Auth check passed, let's go.
|
||||
next.ServeHTTP(rw, r)
|
||||
return
|
||||
})
|
||||
}
|
||||
}
|
113
web/fup/fuphttp/auth_test.go
Normal file
113
web/fup/fuphttp/auth_test.go
Normal file
|
@ -0,0 +1,113 @@
|
|||
package fuphttp_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"hg.lukegb.com/lukegb/depot/web/fup/fuphttp"
|
||||
)
|
||||
|
||||
func TestTokenAuthMiddleware(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
ccfg := *cfg
|
||||
ccfg.AuthMiddleware = fuphttp.TokenAuthMiddleware("token", "realm")
|
||||
|
||||
a, err := fuphttp.New(ctx, &ccfg)
|
||||
if err != nil {
|
||||
t.Fatalf("fuphttp.New: %v", err)
|
||||
}
|
||||
s := httptest.NewServer(a.Handler())
|
||||
t.Cleanup(s.Close)
|
||||
|
||||
tcs := []struct {
|
||||
name string
|
||||
path string
|
||||
username, password string
|
||||
wantStatus int
|
||||
wantText string
|
||||
}{{
|
||||
name: "root, no creds",
|
||||
path: "/",
|
||||
wantStatus: http.StatusUnauthorized,
|
||||
wantText: "unparsable or no credentials\n",
|
||||
}, {
|
||||
name: "root, with bad creds",
|
||||
path: "/",
|
||||
password: "wrong password",
|
||||
wantStatus: http.StatusUnauthorized,
|
||||
wantText: "bad credentials\n",
|
||||
}, {
|
||||
name: "root, with good creds",
|
||||
path: "/",
|
||||
password: "token",
|
||||
wantStatus: http.StatusOK,
|
||||
}, {
|
||||
name: "raw",
|
||||
path: "/raw/foo.txt",
|
||||
wantStatus: http.StatusNotFound,
|
||||
}, {
|
||||
name: "raw, with bad creds",
|
||||
path: "/raw/foo.txt",
|
||||
password: "wrong password",
|
||||
wantStatus: http.StatusNotFound,
|
||||
}, {
|
||||
name: "raw, with good creds",
|
||||
path: "/raw/foo.txt",
|
||||
password: "token",
|
||||
wantStatus: http.StatusNotFound,
|
||||
}, {
|
||||
name: "pretty",
|
||||
path: "/foo.txt",
|
||||
wantStatus: http.StatusNotFound,
|
||||
}, {
|
||||
name: "pretty, with bad creds",
|
||||
path: "/foo.txt",
|
||||
password: "wrong password",
|
||||
wantStatus: http.StatusNotFound,
|
||||
}, {
|
||||
name: "pretty, with good creds",
|
||||
path: "/foo.txt",
|
||||
password: "token",
|
||||
wantStatus: http.StatusNotFound,
|
||||
}}
|
||||
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", s.URL+tc.path, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("NewRequestWithContext: %v", err)
|
||||
}
|
||||
|
||||
if tc.password != "" {
|
||||
req.SetBasicAuth("", tc.password)
|
||||
}
|
||||
|
||||
resp, err := s.Client().Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Do(%q): %v", s.URL+tc.path, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != tc.wantStatus {
|
||||
t.Errorf("StatusCode = %v; want %v", resp.StatusCode, tc.wantStatus)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadAll(Body): %v", err)
|
||||
}
|
||||
|
||||
if tc.wantText != "" && string(body) != tc.wantText {
|
||||
t.Errorf("response body = %q; want %q", string(body), tc.wantText)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue