diff --git a/.gitignore b/.gitignore index 5ae4bb7..4fb0884 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ # Config files providers.yml +auth.yml # Binary nas diff --git a/assets/config_examples/auth.yml b/assets/config_examples/auth.yml new file mode 100644 index 0000000..9e10a9f --- /dev/null +++ b/assets/config_examples/auth.yml @@ -0,0 +1,3 @@ +provider_url: "http://localhost:8080" +realm: "nas" +redirect_base_url: "http://localhost:3000" \ No newline at end of file diff --git a/authentication/keycloak.go b/authentication/keycloak.go new file mode 100644 index 0000000..d026b53 --- /dev/null +++ b/authentication/keycloak.go @@ -0,0 +1,23 @@ +package authentication + +import ( + "fmt" + "github.com/Nerzal/gocloak/v5" +) + +var AuthConfig map[string]string + +func HasAuth(accessToken string) (success bool) { + client := gocloak.NewClient(AuthConfig["provider_url"]) + _, err := client.GetUserInfo(accessToken, AuthConfig["realm"]) + if err != nil { + return false + } + return true +} + +func GetLoginLink() (url string) { + baseString := "%v/auth/realms/%v/protocol/openid-connect/auth?client_id=account&response_mode=fragment&response_type=token&login=true&redirect_uri=%v/api/auth/callback" + authUrl := fmt.Sprintf(baseString, AuthConfig["provider_url"], AuthConfig["realm"], AuthConfig["redirect_base_url"]) + return authUrl +} \ No newline at end of file diff --git a/go.mod b/go.mod index 40f5d1c..a40e545 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,10 @@ module github.com/gmemstr/nas go 1.13 require ( + github.com/Nerzal/gocloak/v5 v5.1.0 github.com/go-yaml/yaml v2.1.0+incompatible github.com/gorilla/mux v1.7.4 github.com/kr/pretty v0.2.0 // indirect - github.com/mattn/go-sqlite3 v2.0.3+incompatible - golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/yaml.v2 v2.2.8 // indirect ) diff --git a/go.sum b/go.sum index 9932cbc..9094e9c 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,12 @@ +github.com/Nerzal/gocloak/v5 v5.1.0 h1:1YP4+GoY1DZ1k7WyNNr8xbFyt55B9ORn2ZHu+XqUK0Q= +github.com/Nerzal/gocloak/v5 v5.1.0/go.mod h1:8v53okuWiWXOKOS6qil8cOn7+5JSQfX1t1d+Nj8FpYk= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/go-resty/resty/v2 v2.0.0 h1:9Nq/U+V4xsoDnDa/iTrABDWUCuk3Ne92XFHPe6dKWUc= +github.com/go-resty/resty/v2 v2.0.0/go.mod h1:dZGr0i9PLlaaTD4H/hoZIDjQ+r6xq8mgbRzHZf7f2J8= github.com/go-yaml/yaml v2.1.0+incompatible h1:RYi2hDdss1u4YE7GwixGzWwVo47T8UQwnTLB6vQiq+o= github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= @@ -7,17 +16,22 @@ github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= -github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d h1:1ZiEyfaQIg3Qh0EoqpwAakHVhecoE5wlSg5GjnafJGw= -golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7 h1:rTIdg5QFRR7XCaK4LCjBiPbx8j4DQRpdYMnGn/bJUEU= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/router/authrouter.go b/router/authrouter.go new file mode 100644 index 0000000..934fe39 --- /dev/null +++ b/router/authrouter.go @@ -0,0 +1,54 @@ +package router + +import ( + "fmt" + "github.com/gmemstr/nas/authentication" + "io/ioutil" + "net/http" +) + +var AuthEnabled bool = true + +func requiresAuth() Handler { + return func(context *Context, w http.ResponseWriter, r *http.Request) *HTTPError { + if !AuthEnabled { + return nil + } + cookie, err := r.Cookie("NAS-SESSION") + if err != nil || !authentication.HasAuth(cookie.Value) { + if err != nil { + fmt.Println("Error", err.Error()) + } + http.Redirect(w, r, authentication.GetLoginLink(), 307) + return &HTTPError{ + Message: "Unauthorized! Redirecting to /login", + StatusCode: http.StatusTemporaryRedirect, + } + } + return nil + } +} + +func callbackAuth() Handler { + return func(context *Context, w http.ResponseWriter, r *http.Request) *HTTPError { + // Translate callback GET to POST to set cookie, then redirect. + if r.Method == "GET" { + javascript := ` +` + w.Write([]byte(javascript)) + return nil + } + token, _ := ioutil.ReadAll(r.Body) + + // Set as HttpOnly cookie to mitigate XSS risk. + jwtCookie := http.Cookie{Name: "NAS-SESSION", + Value: string(token), + HttpOnly: true, + Path: "/", + } + + http.SetCookie(w, &jwtCookie) + + return nil + } +} \ No newline at end of file diff --git a/router/router.go b/router/router.go index 9e46804..18961ae 100644 --- a/router/router.go +++ b/router/router.go @@ -47,21 +47,31 @@ func Init() *mux.Router { // Paths that require specific handlers r.Handle("/", Handle( + requiresAuth(), rootHandler(), )).Methods("GET") + // File & Provider API r.Handle("/api/providers", Handle( + requiresAuth(), ListProviders(), )).Methods("GET") r.Handle(`/api/files/{provider:[a-zA-Z0-9]+\/*}`, Handle( + requiresAuth(), HandleProvider(), )).Methods("GET", "POST") r.Handle(`/api/files/{provider:[a-zA-Z0-9]+}/{file:.+}`, Handle( + requiresAuth(), HandleProvider(), )).Methods("GET", "POST", "DELETE") + // Auth API & Endpoints + r.Handle(`/api/auth/callback`, Handle( + callbackAuth(), + )).Methods("GET", "POST") + return r } diff --git a/webserver.go b/webserver.go index 4614a8a..c465fdc 100644 --- a/webserver.go +++ b/webserver.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "github.com/gmemstr/nas/authentication" "github.com/gmemstr/nas/files" "github.com/gmemstr/nas/router" "github.com/go-yaml/yaml" @@ -23,6 +24,20 @@ func main() { } files.SetupProviders() + // Initialize auth if set up. + authConfig, err := ioutil.ReadFile("auth.yml") + if err != nil { + fmt.Println("!! No Keycloack configuration found !!\n!! Requests will be unauthenticated !!") + router.AuthEnabled = false + } else { + err = yaml.Unmarshal(authConfig, &authentication.AuthConfig) + if err != nil { + fmt.Println("Unable to parse auth.yml file, is it correct?") + router.AuthEnabled = false + } + fmt.Println("Keycloak configured") + } + r := router.Init() fmt.Println("Your NAS instance is live on port :3000") log.Fatal(http.ListenAndServe(":3000", r))