From c559f28ebb0668b0b037554a65a8ac56455c6d40 Mon Sep 17 00:00:00 2001 From: Gabriel Simmer Date: Thu, 16 Apr 2020 23:49:35 +0100 Subject: [PATCH] Authentication handling with Keycloak. Implemented a rudementary authentication method using Keycloak as the IdP - still very barebones, but login does function. Next steps will include a Docker Compose file (most likely) for managing this integration. The application will work fine without setting up the integration however, and will just throw a warning message. Setup should be relatively self explanatory, but some documentation is TBD, along with some automation when spinning up for the first time. Still not super happy with the implementation. --- .gitignore | 1 + assets/config_examples/auth.yml | 3 ++ authentication/keycloak.go | 23 ++++++++++++++ go.mod | 3 +- go.sum | 26 ++++++++++++---- router/authrouter.go | 54 +++++++++++++++++++++++++++++++++ router/router.go | 10 ++++++ webserver.go | 15 +++++++++ 8 files changed, 127 insertions(+), 8 deletions(-) create mode 100644 assets/config_examples/auth.yml create mode 100644 authentication/keycloak.go create mode 100644 router/authrouter.go 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))