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:
parent
a2e00d06e0
commit
456274c1b9
|
@ -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 {
|
|
@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
27
server.go
27
server.go
|
@ -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)
|
||||||
|
|
12
upload.go
12
upload.go
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue