Split and move auth into a separate package (#224)

* Split and move auth into a separate package

This change will make it easier to implement additional authentication
methods, such as OpenID Connect. For now, only the existing "apikeys"
authentication method is supported.

* Use absolute site prefix to prevent redirect loop
This commit is contained in:
mutantmonkey 2020-08-14 07:42:45 +00:00 committed by GitHub
parent a2e00d06e0
commit 456274c1b9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 68 additions and 56 deletions

View File

@ -1,4 +1,4 @@
package main package apikeys
import ( import (
"bufio" "bufio"
@ -24,16 +24,18 @@ const (
type AuthOptions struct { type AuthOptions struct {
AuthFile string AuthFile string
UnauthMethods []string UnauthMethods []string
BasicAuth bool
SiteName string
SitePath string
} }
type auth struct { type ApiKeysMiddleware struct {
successHandler http.Handler successHandler http.Handler
failureHandler http.Handler
authKeys []string authKeys []string
o AuthOptions o AuthOptions
} }
func readAuthKeys(authFile string) []string { func ReadAuthKeys(authFile string) []string {
var authKeys []string var authKeys []string
f, err := os.Open(authFile) f, err := os.Open(authFile)
@ -55,7 +57,7 @@ func readAuthKeys(authFile string) []string {
return authKeys return authKeys
} }
func checkAuth(authKeys []string, key string) (result bool, err error) { func CheckAuth(authKeys []string, key string) (result bool, err error) {
checkKey, err := scrypt.Key([]byte(key), []byte(scryptSalt), scryptN, scryptr, scryptp, scryptKeyLen) checkKey, err := scrypt.Key([]byte(key), []byte(scryptSalt), scryptN, scryptr, scryptp, scryptKeyLen)
if err != nil { if err != nil {
return return
@ -73,53 +75,74 @@ func checkAuth(authKeys []string, key string) (result bool, err error) {
return return
} }
func (a auth) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (a ApiKeysMiddleware) getSitePrefix() string {
if sliceContains(a.o.UnauthMethods, r.Method) { prefix := a.o.SitePath
if len(prefix) <= 0 || prefix[0] != '/' {
prefix = "/" + prefix
}
return prefix
}
func (a ApiKeysMiddleware) goodAuthorizationHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Location", a.getSitePrefix())
w.WriteHeader(http.StatusFound)
}
func (a ApiKeysMiddleware) badAuthorizationHandler(w http.ResponseWriter, r *http.Request) {
if a.o.BasicAuth {
rs := ""
if a.o.SiteName != "" {
rs = fmt.Sprintf(` realm="%s"`, a.o.SiteName)
}
w.Header().Set("WWW-Authenticate", `Basic`+rs)
}
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
}
func (a ApiKeysMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var successHandler http.Handler
prefix := a.getSitePrefix()
if r.URL.Path == prefix+"auth" {
successHandler = http.HandlerFunc(a.goodAuthorizationHandler)
} else {
successHandler = a.successHandler
}
if sliceContains(a.o.UnauthMethods, r.Method) && r.URL.Path != prefix+"auth" {
// allow unauthenticated methods // allow unauthenticated methods
a.successHandler.ServeHTTP(w, r) successHandler.ServeHTTP(w, r)
return return
} }
key := r.Header.Get("Linx-Api-Key") key := r.Header.Get("Linx-Api-Key")
if key == "" && Config.basicAuth { if key == "" && a.o.BasicAuth {
_, password, ok := r.BasicAuth() _, password, ok := r.BasicAuth()
if ok { if ok {
key = password key = password
} }
} }
result, err := checkAuth(a.authKeys, key) result, err := CheckAuth(a.authKeys, key)
if err != nil || !result { if err != nil || !result {
a.failureHandler.ServeHTTP(w, r) http.HandlerFunc(a.badAuthorizationHandler).ServeHTTP(w, r)
return return
} }
a.successHandler.ServeHTTP(w, r) successHandler.ServeHTTP(w, r)
} }
func UploadAuth(o AuthOptions) func(*web.C, http.Handler) http.Handler { func NewApiKeysMiddleware(o AuthOptions) func(*web.C, http.Handler) http.Handler {
fn := func(c *web.C, h http.Handler) http.Handler { fn := func(c *web.C, h http.Handler) http.Handler {
return auth{ return ApiKeysMiddleware{
successHandler: h, successHandler: h,
failureHandler: http.HandlerFunc(badAuthorizationHandler), authKeys: ReadAuthKeys(o.AuthFile),
authKeys: readAuthKeys(o.AuthFile),
o: o, o: o,
} }
} }
return fn return fn
} }
func badAuthorizationHandler(w http.ResponseWriter, r *http.Request) {
if Config.basicAuth {
rs := ""
if Config.siteName != "" {
rs = fmt.Sprintf(` realm="%s"`, Config.siteName)
}
w.Header().Set("WWW-Authenticate", `Basic`+rs)
}
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
}
func sliceContains(slice []string, s string) bool { func sliceContains(slice []string, s string) bool {
for _, v := range slice { for _, v := range slice {
if s == v { if s == v {

View File

@ -1,4 +1,4 @@
package main package apikeys
import ( import (
"testing" "testing"
@ -10,15 +10,15 @@ func TestCheckAuth(t *testing.T) {
"vFpNprT9wbHgwAubpvRxYCCpA2FQMAK6hFqPvAGrdZo=", "vFpNprT9wbHgwAubpvRxYCCpA2FQMAK6hFqPvAGrdZo=",
} }
if r, err := checkAuth(authKeys, ""); err != nil && r { if r, err := CheckAuth(authKeys, ""); err != nil && r {
t.Fatal("Authorization passed for empty key") t.Fatal("Authorization passed for empty key")
} }
if r, err := checkAuth(authKeys, "thisisnotvalid"); err != nil && r { if r, err := CheckAuth(authKeys, "thisisnotvalid"); err != nil && r {
t.Fatal("Authorization passed for invalid key") t.Fatal("Authorization passed for invalid key")
} }
if r, err := checkAuth(authKeys, "haPVipRnGJ0QovA9nyqK"); err != nil && !r { if r, err := CheckAuth(authKeys, "haPVipRnGJ0QovA9nyqK"); err != nil && !r {
t.Fatal("Authorization failed for valid key") t.Fatal("Authorization failed for valid key")
} }
} }

View File

@ -16,6 +16,7 @@ import (
"time" "time"
rice "github.com/GeertJohan/go.rice" rice "github.com/GeertJohan/go.rice"
"github.com/andreimarcu/linx-server/auth/apikeys"
"github.com/andreimarcu/linx-server/backends" "github.com/andreimarcu/linx-server/backends"
"github.com/andreimarcu/linx-server/backends/localfs" "github.com/andreimarcu/linx-server/backends/localfs"
"github.com/andreimarcu/linx-server/backends/s3" "github.com/andreimarcu/linx-server/backends/s3"
@ -110,9 +111,12 @@ func setup() *web.Mux {
mux.Use(AddHeaders(Config.addHeaders)) mux.Use(AddHeaders(Config.addHeaders))
if Config.authFile != "" { if Config.authFile != "" {
mux.Use(UploadAuth(AuthOptions{ mux.Use(apikeys.NewApiKeysMiddleware(apikeys.AuthOptions{
AuthFile: Config.authFile, AuthFile: Config.authFile,
UnauthMethods: []string{"GET", "HEAD", "OPTIONS", "TRACE"}, UnauthMethods: []string{"GET", "HEAD", "OPTIONS", "TRACE"},
BasicAuth: Config.basicAuth,
SiteName: Config.siteName,
SitePath: Config.sitePath,
})) }))
} }
@ -196,29 +200,10 @@ func setup() *web.Mux {
mux.Get(Config.sitePath+"upload/", uploadRemote) mux.Get(Config.sitePath+"upload/", uploadRemote)
if Config.remoteAuthFile != "" { if Config.remoteAuthFile != "" {
remoteAuthKeys = readAuthKeys(Config.remoteAuthFile) remoteAuthKeys = apikeys.ReadAuthKeys(Config.remoteAuthFile)
} }
} }
if Config.basicAuth {
options := AuthOptions{
AuthFile: Config.authFile,
UnauthMethods: []string{},
}
okFunc := func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Location", Config.sitePath)
w.WriteHeader(http.StatusFound)
}
authHandler := auth{
successHandler: http.HandlerFunc(okFunc),
failureHandler: http.HandlerFunc(badAuthorizationHandler),
authKeys: readAuthKeys(Config.authFile),
o: options,
}
mux.Head(Config.sitePath+"auth", authHandler)
mux.Get(Config.sitePath+"auth", authHandler)
}
mux.Post(Config.sitePath+"upload", uploadPostHandler) mux.Post(Config.sitePath+"upload", uploadPostHandler)
mux.Post(Config.sitePath+"upload/", uploadPostHandler) mux.Post(Config.sitePath+"upload/", uploadPostHandler)
mux.Put(Config.sitePath+"upload", uploadPutHandler) mux.Put(Config.sitePath+"upload", uploadPutHandler)

View File

@ -15,6 +15,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/andreimarcu/linx-server/auth/apikeys"
"github.com/andreimarcu/linx-server/backends" "github.com/andreimarcu/linx-server/backends"
"github.com/andreimarcu/linx-server/expiry" "github.com/andreimarcu/linx-server/expiry"
"github.com/dchest/uniuri" "github.com/dchest/uniuri"
@ -166,13 +167,16 @@ func uploadRemote(c web.C, w http.ResponseWriter, r *http.Request) {
key = password key = password
} }
} }
result, err := checkAuth(remoteAuthKeys, key) result, err := apikeys.CheckAuth(remoteAuthKeys, key)
if err != nil || !result { if err != nil || !result {
if Config.basicAuth { if Config.basicAuth {
badAuthorizationHandler(w, r) rs := ""
} else { if Config.siteName != "" {
unauthorizedHandler(c, w, r) rs = fmt.Sprintf(` realm="%s"`, Config.siteName)
}
w.Header().Set("WWW-Authenticate", `Basic`+rs)
} }
unauthorizedHandler(c, w, r)
return return
} }
} }