From 52be3a9088b61d9b82dbee725808d3aeaad36472 Mon Sep 17 00:00:00 2001 From: Aaron Parecki Date: Sun, 8 Oct 2017 16:52:58 -0700 Subject: [PATCH 001/360] disambiguate debug messages --- handlers/handlers.go | 4 +++- pkg/domains/domains.go | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/handlers/handlers.go b/handlers/handlers.go index d164a066..46e11d52 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -100,9 +100,11 @@ func loginURL(r *http.Request, state string) string { // See relevant RFC: http://tools.ietf.org/html/rfc6749#section-10.12 var url = "" if gcred.ClientID != "" { + // If the provider is Google, find a matching redirect URL to use for the client domain := domains.Matches(r.Host) + log.Debugf("looking for redirect URL matching %v", domain) for i, v := range gcred.RedirectURLs { - log.Debugf("array value at [%d]=%v", i, v) + log.Debugf("redirect value matched at [%d]=%v", i, v) if strings.Contains(v, domain) { oauthclient.RedirectURL = v break diff --git a/pkg/domains/domains.go b/pkg/domains/domains.go index 076e550e..ce6c8453 100644 --- a/pkg/domains/domains.go +++ b/pkg/domains/domains.go @@ -14,7 +14,7 @@ import ( // TODO return all matches func Matches(s string) string { for i, v := range cfg.Cfg.Domains { - log.Debugf("array value at [%d]=%v", i, v) + log.Debugf("domain matched array value at [%d]=%v", i, v) if strings.Contains(s, v) { return v } From a429e4e55fb0f9c6b64fdd608da29234a2a0ad16 Mon Sep 17 00:00:00 2001 From: Aaron Parecki Date: Sun, 8 Oct 2017 16:53:44 -0700 Subject: [PATCH 002/360] fix redirect URL for generic provider --- pkg/structs/structs.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/structs/structs.go b/pkg/structs/structs.go index a11185d6..5f98f491 100644 --- a/pkg/structs/structs.go +++ b/pkg/structs/structs.go @@ -46,7 +46,7 @@ type GenericOauth struct { ClientSecret string `mapstructure:"client_secret"` AuthURL string `mapstructure:"auth_url"` TokenURL string `mapstructure:"token_url"` - RedirectURL string `mapstructure:"callback_url "` + RedirectURL string `mapstructure:"callback_url"` Scopes []string `mapstructure:"scopes"` UserInfoURL string `mapstructure:"user_info_url"` Provider string `mapstructure:"provider"` From f62bd5f2f4b2b2f40af77955790b0f78adb05727 Mon Sep 17 00:00:00 2001 From: Aaron Parecki Date: Sun, 8 Oct 2017 18:27:13 -0700 Subject: [PATCH 003/360] allow forcing the domain of the cookie to set --- config/config.yml_example | 2 ++ pkg/cfg/cfg.go | 1 + pkg/cookie/cookie.go | 5 +++++ 3 files changed, 8 insertions(+) diff --git a/config/config.yml_example b/config/config.yml_example index 8db4cc42..724eec55 100644 --- a/config/config.yml_example +++ b/config/config.yml_example @@ -21,6 +21,8 @@ lasso: cookie: # name of cookie to store the jwt name: Lasso + # optionally force the domain of the cookie to set + # domain: yourdomain.com secure: false httpOnly: true headers: diff --git a/pkg/cfg/cfg.go b/pkg/cfg/cfg.go index 2c2e4a23..3b135fb6 100644 --- a/pkg/cfg/cfg.go +++ b/pkg/cfg/cfg.go @@ -23,6 +23,7 @@ type CfgT struct { } Cookie struct { Name string `mapstructure:"name"` + Domain string `mapstructure:"domain"` Secure bool `mapstructure:"secure"` HTTPOnly bool `mapstructure:"httpOnly"` } diff --git a/pkg/cookie/cookie.go b/pkg/cookie/cookie.go index 72d3dd3e..e8c9baa5 100644 --- a/pkg/cookie/cookie.go +++ b/pkg/cookie/cookie.go @@ -23,6 +23,11 @@ func setCookie(w http.ResponseWriter, r *http.Request, val string, maxAge int) { maxAge = defaultMaxAge } domain := domains.Matches(r.Host) + // Allow overriding the cookie domain in the config file + if cfg.Cfg.Cookie.Domain != "" { + domain = cfg.Cfg.Cookie.Domain + log.Debugf("setting the cookie domain to %v", domain) + } // log.Debugf("cookie %s expires %d", cfg.Cfg.Cookie.Name, expires) http.SetCookie(w, &http.Cookie{ Name: cfg.Cfg.Cookie.Name, From 1688b08fd2b5e058f82b3e14f2b2bb1f3349ba68 Mon Sep 17 00:00:00 2001 From: Aaron Parecki Date: Sun, 8 Oct 2017 18:30:46 -0700 Subject: [PATCH 004/360] add option to allow all users will not reject any user based on domain matching. useful when using Lasso to identify users rather than determine whether they are authorized. --- config/config.yml_example | 2 ++ handlers/handlers.go | 9 ++++++--- pkg/cfg/cfg.go | 1 + 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/config/config.yml_example b/config/config.yml_example index 8db4cc42..d5626786 100644 --- a/config/config.yml_example +++ b/config/config.yml_example @@ -7,6 +7,8 @@ lasso: logLevel: info listen: 0.0.0.0 port: 9090 + # set allowAllUsers: true to use Lasso to just identify users rather than determine whether they are authorized + allowAllUsers: false # each of these domains must serve the url https://lasso.$domains[0] https://lasso.$domains[1] ... # usually you'll just have one domains: diff --git a/handlers/handlers.go b/handlers/handlers.go index 46e11d52..30451592 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -201,9 +201,11 @@ func ValidateRequestHandler(w http.ResponseWriter, r *http.Request) { } log.Infof("email from jwt cookie: %s", claims.Email) - if !jwtmanager.SiteInClaims(r.Host, &claims) { - error401(w, r, AuthError{"not authorized for " + r.Host, jwt}) - return + if !cfg.Cfg.AllowAllUsers { + if !jwtmanager.SiteInClaims(r.Host, &claims) { + error401(w, r, AuthError{"not authorized for " + r.Host, jwt}) + return + } } // renderIndex(w, "user found from email "+user.Email) @@ -325,6 +327,7 @@ func VerifyUser(u interface{}) (ok bool, err error) { // } else if !domains.IsUnderManagement(user.HostDomain) { // err = fmt.Errorf("HostDomain %s is not within a lasso managed domain", u.HostDomain) } else { + log.Debugf("no domains configured") ok = true } return ok, err diff --git a/pkg/cfg/cfg.go b/pkg/cfg/cfg.go index 2c2e4a23..5bae7274 100644 --- a/pkg/cfg/cfg.go +++ b/pkg/cfg/cfg.go @@ -15,6 +15,7 @@ type CfgT struct { Listen string `mapstructure:"listen"` Port int `mapstructure:"port"` Domains []string `mapstructure:"domains"` + AllowAllUsers bool `mapstructure:"allowAllUsers"` JWT struct { MaxAge int `mapstructure:"maxAge"` Issuer string `mapstructure:"issuer"` From de8e608275cddf7f25904a3565fc3df71bd8fa29 Mon Sep 17 00:00:00 2001 From: Aaron Parecki Date: Sun, 8 Oct 2017 20:32:22 -0700 Subject: [PATCH 005/360] add config option to allow public access setting `publicAccess: true` tells Lasso to allow requests even without a cookie. this is useful for public sites that also allow users to sign in. --- config/config.yml_example | 2 ++ handlers/handlers.go | 7 ++++++- pkg/cfg/cfg.go | 1 + 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/config/config.yml_example b/config/config.yml_example index 8db4cc42..fdc692ae 100644 --- a/config/config.yml_example +++ b/config/config.yml_example @@ -7,6 +7,8 @@ lasso: logLevel: info listen: 0.0.0.0 port: 9090 + # Setting publicAccess: true will accept all requests, even without a cookie. If the user is logged in, the cookie will be validated and the user header will be set. + publicAccess: false # each of these domains must serve the url https://lasso.$domains[0] https://lasso.$domains[1] ... # usually you'll just have one domains: diff --git a/handlers/handlers.go b/handlers/handlers.go index 46e11d52..0adf35ab 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -184,7 +184,12 @@ func ValidateRequestHandler(w http.ResponseWriter, r *http.Request) { jwt := FindJWT(r) // if jwt != "" { if jwt == "" { - error401(w, r, AuthError{Error: "no jwt found"}) + // If the module is configured to allow public access with no authentication, return 200 now + if !cfg.Cfg.PublicAccess { + error401(w, r, AuthError{Error: "no jwt found"}) + } else { + w.Header().Add("X-Lasso-User", ""); + } return } diff --git a/pkg/cfg/cfg.go b/pkg/cfg/cfg.go index 2c2e4a23..83724a78 100644 --- a/pkg/cfg/cfg.go +++ b/pkg/cfg/cfg.go @@ -15,6 +15,7 @@ type CfgT struct { Listen string `mapstructure:"listen"` Port int `mapstructure:"port"` Domains []string `mapstructure:"domains"` + PublicAccess bool `mapstructure:"publicAccess"` JWT struct { MaxAge int `mapstructure:"maxAge"` Issuer string `mapstructure:"issuer"` From c2ff2dc9e95ac0f65107865c721752df4a839c02 Mon Sep 17 00:00:00 2001 From: Benjamin Foote Date: Tue, 10 Oct 2017 05:48:37 -0700 Subject: [PATCH 006/360] document X-Lasso-User --- README.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0235ee2c..414a2e1e 100644 --- a/README.md +++ b/README.md @@ -35,10 +35,8 @@ server { proxy_pass_request_body off; proxy_set_header Content-Length ""; - # not currently - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; + # pass X-Lasso-User along with the request + auth_request_set $auth_resp_x_lasso_user $upstream_http_x_lasso_user; # these return values are used by the @error401 call auth_request_set $auth_resp_jwt $upstream_http_x_lasso_jwt; @@ -50,6 +48,13 @@ server { # redirect to lasso for login return 302 https://lasso.yourdomain.com:9090/login?url=$scheme://$http_host$request_uri&lasso-failcount=$auth_resp_failcount&X-Lasso-Token=$auth_resp_jwt&error=$auth_resp_err; } + + # proxy pass authorized requests to your service + location / { + proxy_pass http://dev.yourdomain.com:8080; + # set user header (usually an email) + proxy_set_header X-Lasso-User $auth_resp_x_lasso_user; + } } ``` From 5271c2a8fd416f67deafe396da5ade1e2c4718fb Mon Sep 17 00:00:00 2001 From: Benjamin Foote Date: Tue, 10 Oct 2017 05:48:57 -0700 Subject: [PATCH 007/360] run, build --- do.sh | 46 ++++++++++++++-------------------------------- 1 file changed, 14 insertions(+), 32 deletions(-) diff --git a/do.sh b/do.sh index 779d51cf..78afcf5d 100755 --- a/do.sh +++ b/do.sh @@ -14,6 +14,14 @@ NAME=lasso HTTPPORT=9090 GODOC_PORT=5050 +run () { + go run main.go +} + +build () { + go build . +} + gogo () { docker run --rm -i -t -v /var/run/docker.sock:/var/run/docker.sock -v ${SDIR}/go:/go --name gogo $GOIMAGE $* } @@ -112,14 +120,16 @@ browsebolt() { usage() { cat < lasso_flow.jpg + $0 graphviz - lasso_flow.dot --> lasso_flow.jpg do is like make @@ -130,41 +140,13 @@ EOF ARG=$1; shift; case "$ARG" in - 'browsebolt') - browsebolt - ;; - - 'build') - dbuild - ;; - 'drun') - drun $* - ;; - 'revproxy') - revproxy $* - ;; - 'graphviz') - graphviz $* - ;; - 'test') - test $* + 'run'|'build'|'browsebolt'|'dbuild'|'drun'|'revproxy'|'graphviz'|'test'|'goget'|'gogo'|'watch'|'gobuildstatic') + $ARG $* ;; 'godoc') echo "godoc running at http://${GODOC_PORT}" godoc -http=:${GODOC_PORT} ;; - 'goget'|'get') - goget $* - ;; - 'gogo') - gogo $* - ;; - 'watch') - watch $* - ;; - 'gobuildstatic') - gobuildstatic $* - ;; 'all') gobuildstatic dbuild From 16e27280d816d59088d54cb17f15d62531b7646e Mon Sep 17 00:00:00 2001 From: Benjamin Foote Date: Wed, 18 Oct 2017 17:30:45 -0700 Subject: [PATCH 008/360] commit fixed conficts --- config/config.yml_example | 3 --- pkg/cfg/cfg.go | 17 +++++++---------- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/config/config.yml_example b/config/config.yml_example index cc61fa7b..a4b26817 100644 --- a/config/config.yml_example +++ b/config/config.yml_example @@ -7,13 +7,10 @@ lasso: logLevel: info listen: 0.0.0.0 port: 9090 -<<<<<<< HEAD # set allowAllUsers: true to use Lasso to just identify users rather than determine whether they are authorized allowAllUsers: false -======= # Setting publicAccess: true will accept all requests, even without a cookie. If the user is logged in, the cookie will be validated and the user header will be set. publicAccess: false ->>>>>>> de8e608275cddf7f25904a3565fc3df71bd8fa29 # each of these domains must serve the url https://lasso.$domains[0] https://lasso.$domains[1] ... # usually you'll just have one domains: diff --git a/pkg/cfg/cfg.go b/pkg/cfg/cfg.go index 2738a914..9b936f59 100644 --- a/pkg/cfg/cfg.go +++ b/pkg/cfg/cfg.go @@ -11,16 +11,13 @@ import ( // CfgT lasso jwt cookie configuration type CfgT struct { - LogLevel string `mapstructure:"logLevel"` - Listen string `mapstructure:"listen"` - Port int `mapstructure:"port"` - Domains []string `mapstructure:"domains"` -<<<<<<< HEAD - AllowAllUsers bool `mapstructure:"allowAllUsers"` -======= - PublicAccess bool `mapstructure:"publicAccess"` ->>>>>>> de8e608275cddf7f25904a3565fc3df71bd8fa29 - JWT struct { + LogLevel string `mapstructure:"logLevel"` + Listen string `mapstructure:"listen"` + Port int `mapstructure:"port"` + Domains []string `mapstructure:"domains"` + AllowAllUsers bool `mapstructure:"allowAllUsers"` + PublicAccess bool `mapstructure:"publicAccess"` + JWT struct { MaxAge int `mapstructure:"maxAge"` Issuer string `mapstructure:"issuer"` Secret string `mapstructure:"secret"` From 3ae7e891e3398e73987758eb4d6d3c55897e63cb Mon Sep 17 00:00:00 2001 From: Aaron Parecki Date: Sat, 2 Dec 2017 13:35:45 -0800 Subject: [PATCH 009/360] add `url` param to logout route allows redirecting elsewhere when logging out --- handlers/handlers.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/handlers/handlers.go b/handlers/handlers.go index eaa85ef3..70dec5f4 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -256,7 +256,13 @@ func LogoutHandler(w http.ResponseWriter, r *http.Request) { } session.Save(r, w) sessstore.MaxAge(300) - renderIndex(w, "you have been logged out") + + var redirectURL = r.URL.Query().Get("url") + if redirectURL != "" { + http.Redirect(w, r, redirectURL, 302); + } else { + renderIndex(w, "you have been logged out") + } } // LoginHandler /login From 55f72298e147fef73c1856cbe689bb05ce1f86cd Mon Sep 17 00:00:00 2001 From: Aaron Parecki Date: Mon, 4 Dec 2017 07:28:03 -0800 Subject: [PATCH 010/360] don't crash on invalid cookie data --- pkg/jwtmanager/jwtmanager.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/jwtmanager/jwtmanager.go b/pkg/jwtmanager/jwtmanager.go index 1d38d3c7..58865906 100644 --- a/pkg/jwtmanager/jwtmanager.go +++ b/pkg/jwtmanager/jwtmanager.go @@ -170,16 +170,16 @@ func decodeAndDecompressTokenString(encgzipss string) string { // gzipss, err := url.QueryUnescape(encgzipss) gzipss, err := base64.URLEncoding.DecodeString(encgzipss) if err != nil { - log.Fatal(err) + log.Debugf("Error in Base64decode: %v", err) } breader := bytes.NewReader(gzipss) zr, err := gzip.NewReader(breader) if err != nil { - log.Fatal(err) + log.Debugf("Error reading gzip data: %v", err) } if err := zr.Close(); err != nil { - log.Fatal(err) + log.Debugf("Error decoding token: %v", err) } ss, _ := ioutil.ReadAll(zr) return string(ss) From 055c166881d13d0eb40add607b008c5ff2b1ba5b Mon Sep 17 00:00:00 2001 From: Aaron Parecki Date: Mon, 4 Dec 2017 07:58:42 -0800 Subject: [PATCH 011/360] don't continue processing on invalid gzip data closes #10 --- pkg/jwtmanager/jwtmanager.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/jwtmanager/jwtmanager.go b/pkg/jwtmanager/jwtmanager.go index 58865906..d9d61e05 100644 --- a/pkg/jwtmanager/jwtmanager.go +++ b/pkg/jwtmanager/jwtmanager.go @@ -177,6 +177,7 @@ func decodeAndDecompressTokenString(encgzipss string) string { zr, err := gzip.NewReader(breader) if err != nil { log.Debugf("Error reading gzip data: %v", err) + return "" } if err := zr.Close(); err != nil { log.Debugf("Error decoding token: %v", err) From 90d140c2ee3483fb1cfe5af496a8c89e60a062be Mon Sep 17 00:00:00 2001 From: Aaron Parecki Date: Mon, 4 Dec 2017 08:05:33 -0800 Subject: [PATCH 012/360] use OAuth 2.0 Bearer Token when looking for token Modifies `FindJWT` function to also look in the `Authorization` header for an OAuth 2.0 Bearer Token, e.g. `Authorization: Bearer XXXXXXX` Adds a new config item to set the query string param separately from the HTTP header, so if you set it to `access_token` then it also follows the Bearer Token spec. --- config/config.yml_example | 3 ++- handlers/handlers.go | 37 ++++++++++++++++++++++++------------- pkg/cfg/cfg.go | 3 ++- 3 files changed, 28 insertions(+), 15 deletions(-) diff --git a/config/config.yml_example b/config/config.yml_example index a4b26817..9a24a4dd 100644 --- a/config/config.yml_example +++ b/config/config.yml_example @@ -30,7 +30,8 @@ lasso: secure: false httpOnly: true headers: - sso: X-Lasso-Token + jwt: X-Lasso-Token + querystring: access_token redirect: X-Lasso-Requested-URI db: file: data/lasso_bolt.db diff --git a/handlers/handlers.go b/handlers/handlers.go index 70dec5f4..d1cfcccb 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -119,23 +119,34 @@ func loginURL(r *http.Request, state string) string { return url } -// FindJWT look for JWT in Cookie, Header and Query String in that order +// FindJWT look for JWT in Cookie, JWT Header, Authorization Header (OAuth 2 Bearer Token) +// and Query String in that order func FindJWT(r *http.Request) string { jwt, err := cookie.Cookie(r) - if err != nil { - log.Error(err) - // return "" - } - log.Debugf("jwtCookie from cookie: %s", jwt) - if jwt == "" { - jwt = r.Header.Get(cfg.Cfg.Headers.SSO) - log.Debugf("jwtCookie from header %s: %s", cfg.Cfg.Headers.SSO, jwt) + if err == nil { + log.Debugf("jwt from cookie: %s", jwt) + return jwt + } + jwt = r.Header.Get(cfg.Cfg.Headers.JWT) + if jwt != "" { + log.Debugf("jwt from header %s: %s", cfg.Cfg.Headers.JWT, jwt) + return jwt + } + auth := r.Header.Get("Authorization") + if auth != "" { + s := strings.SplitN(auth, " ", 2) + if len(s) == 2 { + jwt = s[1] + log.Debugf("jwt from authorization header: %s", jwt) + return jwt + } } - if jwt == "" { - jwt = r.URL.Query().Get(cfg.Cfg.Headers.SSO) - log.Debugf("jwtCookie from querystring %s: %s", cfg.Cfg.Headers.SSO, jwt) + jwt = r.URL.Query().Get(cfg.Cfg.Headers.QueryString) + if jwt != "" { + log.Debugf("jwt from querystring %s: %s", cfg.Cfg.Headers.QueryString, jwt) + return jwt } - return jwt + return "" } // ClaimsFromJWT look everywhere for the JWT, then parse the jwt and return the claims diff --git a/pkg/cfg/cfg.go b/pkg/cfg/cfg.go index 9b936f59..41b90e24 100644 --- a/pkg/cfg/cfg.go +++ b/pkg/cfg/cfg.go @@ -30,7 +30,8 @@ type CfgT struct { HTTPOnly bool `mapstructure:"httpOnly"` } Headers struct { - SSO string `mapstructure:"sso"` + JWT string `mapstructure:"jwt"` + QueryString string `mapstructure:"querystring"` Redirect string `mapstructure:"redirect"` } DB struct { From c21f61cafa9240e61f0390d0f8e200c8505bdfe1 Mon Sep 17 00:00:00 2001 From: Aaron Parecki Date: Thu, 7 Dec 2017 11:04:45 -0800 Subject: [PATCH 013/360] fix for PublicAccess=true was previously returning 401 when an invalid JWT was sent, rather than allowing but setting the user to a blank string --- handlers/handlers.go | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/handlers/handlers.go b/handlers/handlers.go index 70dec5f4..9d9c72ba 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -196,19 +196,31 @@ func ValidateRequestHandler(w http.ResponseWriter, r *http.Request) { claims, err := ClaimsFromJWT(jwt) if err != nil { // no email in jwt - error401(w, r, AuthError{err.Error(), jwt}) + if !cfg.Cfg.PublicAccess { + error401(w, r, AuthError{err.Error(), jwt}) + } else { + w.Header().Add("X-Lasso-User", ""); + } return } if claims.Email == "" { // no email in jwt - error401(w, r, AuthError{"no email found in jwt", jwt}) + if !cfg.Cfg.PublicAccess { + error401(w, r, AuthError{"no email found in jwt", jwt}) + } else { + w.Header().Add("X-Lasso-User", ""); + } return } log.Infof("email from jwt cookie: %s", claims.Email) if !cfg.Cfg.AllowAllUsers { if !jwtmanager.SiteInClaims(r.Host, &claims) { - error401(w, r, AuthError{"not authorized for " + r.Host, jwt}) + if !cfg.Cfg.PublicAccess { + error401(w, r, AuthError{"not authorized for " + r.Host, jwt}) + } else { + w.Header().Add("X-Lasso-User", ""); + } return } } From 6f3056c02e9df8aa2a472ac50a0555d95a892d7a Mon Sep 17 00:00:00 2001 From: Aaron Parecki Date: Fri, 10 Aug 2018 16:11:45 -0700 Subject: [PATCH 014/360] add support for OpenID Connect providers --- handlers/handlers.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/handlers/handlers.go b/handlers/handlers.go index fa380c48..f930a5f9 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -450,6 +450,28 @@ func getUserInfo(r *http.Request, user *structs.User) error { return getUserInfoFromGoogle(client, user) } else if genOauth.Provider == "github" { return getUserInfoFromGithub(client, user, providerToken) + } else if genOauth.Provider == "oidc" { + return getUserInfoFromOpenID(client, user, providerToken) + } else { + log.Error("we don't know how to look up the user info") + } + return nil +} + +func getUserInfoFromOpenID(client *http.Client, user *structs.User, ptoken *oauth2.Token) error { + userinfo, err := client.Get(genOauth.UserInfoURL) + if err != nil { + // http.Error(w, err.Error(), http.StatusBadRequest) + return err + } + defer userinfo.Body.Close() + data, _ := ioutil.ReadAll(userinfo.Body) + log.Println("OpenID userinfo body: ", string(data)) + if err = json.Unmarshal(data, user); err != nil { + log.Errorln(err) + // renderIndex(w, "Error marshalling response. Please try agian.") + // c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{"message": }) + return err } return nil } From f4d440d3d4c44cfd3b6bc7e66ad3fb7338bf154f Mon Sep 17 00:00:00 2001 From: Aaron Parecki Date: Fri, 10 Aug 2018 16:58:03 -0700 Subject: [PATCH 015/360] drop special-casing google credentials * changes config file format to drop the extra level of nesting * update example config with more explanations * Use more different names for lasso cookie vs session to be less confused when looking at the debug logs --- config/config.yml_example | 107 ++++++++++++++++++++------------------ handlers/handlers.go | 67 ++++++++++++------------ pkg/structs/structs.go | 11 +--- 3 files changed, 90 insertions(+), 95 deletions(-) diff --git a/config/config.yml_example b/config/config.yml_example index 9a24a4dd..5611abf4 100644 --- a/config/config.yml_example +++ b/config/config.yml_example @@ -1,22 +1,21 @@ - # lasso config - - lasso: # logLevel: debug logLevel: info listen: 0.0.0.0 port: 9090 - # set allowAllUsers: true to use Lasso to just identify users rather than determine whether they are authorized + # set allowAllUsers: true to use Lasso to just accept anyone who can authenticate at the configured provider allowAllUsers: false - # Setting publicAccess: true will accept all requests, even without a cookie. If the user is logged in, the cookie will be validated and the user header will be set. + # Setting publicAccess: true will accept all requests, even without a cookie. + # If the user is logged in, the cookie will be validated and the user header will be set. + # You will need to direct people to the Lasso login page from your application. publicAccess: false # each of these domains must serve the url https://lasso.$domains[0] https://lasso.$domains[1] ... - # usually you'll just have one + # usually you'll just have one. + # Comment this out if you set allowAllUser:true domains: - yourdomain.com - yourotherdomain.com - # gets sent to google as a primer jwt: issuer: Lasso maxAge: 240 @@ -24,10 +23,10 @@ lasso: compress: true cookie: # name of cookie to store the jwt - name: Lasso + name: Lasso-cookie # optionally force the domain of the cookie to set # domain: yourdomain.com - secure: false + secure: true httpOnly: true headers: jwt: X-Lasso-Token @@ -36,50 +35,56 @@ lasso: db: file: data/lasso_bolt.db session: - name: lasso - test_url: http://my.testing.site.com + name: lasso-session + test_url: http://yourdomain.com # -# OAuth Config +# OAuth Provider Config # oauth: - # configure one of the following - google: - # create new credentials at: - # https://console.developers.google.com/apis/credentials - client_id: - client_secret: - # must be the /auth endpoint - callback_urls: - - http://lasso.yourdomain.com:9090/auth - - http://lasso.yourotherdomain.com:9090/auth - preferredDomain: yourdomain.com - generic: - # create new credentials at: - # https://developer.github.com/apps/building-integrations/setting-up-and-registering-oauth-apps/about-authorization-options-for-oauth-apps/ - provider: github - client_id: - client_secret: - auth_url: https://github.com/login/oauth/authorize - token_url: https://github.com/login/oauth/access_token - # callback_url is configured at github.com when setting up the app - scopes: - - user - user_info_url: https://api.github.com/user?access_token= - generic: - # https://indieauth.com/developers - provider: indieauth - client_id: http://yourdomain.com - # must be the /auth endpoint - auth_url: https://indieauth.com/auth - user_info_url: https://indieauth.com/auth - callback_url: http://lasso.yourdomain.com:9090/auth - generic: - # https://indieauth.com/developers - provider: indieauth - client_id: http://yourdomain.com - # must be the /auth endpoint - auth_url: https://indieauth.com/auth - user_info_url: https://indieauth.com/auth - callback_url: http://lasso.yourdomain.com:9090/auth + # configure only one of the following + + # Google + provider: google + # create new credentials at: + # https://console.developers.google.com/apis/credentials + client_id: + client_secret: + # must be the /auth endpoint + callback_urls: + - http://lasso.yourdomain.com:9090/auth + - http://lasso.yourotherdomain.com:9090/auth + preferredDomain: yourdomain.com + + # GitHub + # https://developer.github.com/apps/building-integrations/setting-up-and-registering-oauth-apps/about-authorization-options-for-oauth-apps/ + provider: github + client_id: + client_secret: + auth_url: https://github.com/login/oauth/authorize + token_url: https://github.com/login/oauth/access_token + # callback_url is configured at github.com when setting up the app + scopes: + - user + user_info_url: https://api.github.com/user?access_token= + + # Generic OpenID Connect + provider: oidc + client_id: + client_secret: + auth_url: https://{yourOktaDomain}/oauth2/default/v1/authorize + token_url: https://{yourOktaDomain}/oauth2/default/v1/token + user_info_url: https://{yourOktaDomain}/oauth2/default/v1/userinfo + scopes: + - openid + - email + - profile + callback_url: http://lasso.yourdomain.com:9090/auth + + # IndieAuth + # https://indielogin.com/api + provider: indieauth + client_id: http://yourdomain.com + auth_url: https://indielogin.com/auth + callback_url: http://lasso.yourdomain.com:9090/auth diff --git a/handlers/handlers.go b/handlers/handlers.go index f930a5f9..018a9846 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -40,7 +40,6 @@ type AuthError struct { } var ( - gcred structs.GCredentials genOauth structs.GenericOauth oauthclient *oauth2.Config oauthopts oauth2.AuthCodeOption @@ -54,37 +53,34 @@ var ( func init() { log.Debug("init handlers") - // if grcred exist - err := cfg.UnmarshalKey("oauth.google", &gcred) - if err == nil && gcred.ClientID != "" { - log.Info("configuring google oauth") - oauthclient = &oauth2.Config{ - ClientID: gcred.ClientID, - ClientSecret: gcred.ClientSecret, - // RedirectURL: gcred.RedirectURL, - Scopes: []string{ - // You have to select a scope from - // https://developers.google.com/identity/protocols/googlescopes#google_sign-in - "https://www.googleapis.com/auth/userinfo.email", - }, - Endpoint: google.Endpoint, - } - log.Infof("setting google oauth prefered login domain param 'hd' to %s", gcred.PreferredDomain) - oauthopts = oauth2.SetAuthURLParam("hd", gcred.PreferredDomain) - return - } - err = cfg.UnmarshalKey("oauth.generic", &genOauth) + err := cfg.UnmarshalKey("oauth", &genOauth) if err == nil { - log.Info("configuring generic oauth") - oauthclient = &oauth2.Config{ - ClientID: genOauth.ClientID, - ClientSecret: genOauth.ClientSecret, - Endpoint: oauth2.Endpoint{ - AuthURL: genOauth.AuthURL, - TokenURL: genOauth.TokenURL, - }, - RedirectURL: genOauth.RedirectURL, - Scopes: genOauth.Scopes, + if genOauth.Provider == "google" { + log.Info("configuring google oauth") + oauthclient = &oauth2.Config{ + ClientID: genOauth.ClientID, + ClientSecret: genOauth.ClientSecret, + Scopes: []string{ + // You have to select a scope from + // https://developers.google.com/identity/protocols/googlescopes#google_sign-in + "https://www.googleapis.com/auth/userinfo.email", + }, + Endpoint: google.Endpoint, + } + log.Infof("setting google oauth preferred login domain param 'hd' to %s", genOauth.PreferredDomain) + oauthopts = oauth2.SetAuthURLParam("hd", genOauth.PreferredDomain) + } else { + log.Info("configuring generic oauth") + oauthclient = &oauth2.Config{ + ClientID: genOauth.ClientID, + ClientSecret: genOauth.ClientSecret, + Endpoint: oauth2.Endpoint{ + AuthURL: genOauth.AuthURL, + TokenURL: genOauth.TokenURL, + }, + RedirectURL: genOauth.RedirectURL, + Scopes: genOauth.Scopes, + } } } } @@ -99,11 +95,11 @@ func loginURL(r *http.Request, state string) string { // State can be some kind of random generated hash string. // See relevant RFC: http://tools.ietf.org/html/rfc6749#section-10.12 var url = "" - if gcred.ClientID != "" { + if genOauth.Provider == "google" { // If the provider is Google, find a matching redirect URL to use for the client domain := domains.Matches(r.Host) log.Debugf("looking for redirect URL matching %v", domain) - for i, v := range gcred.RedirectURLs { + for i, v := range genOauth.RedirectURLs { log.Debugf("redirect value matched at [%d]=%v", i, v) if strings.Contains(v, domain) { oauthclient.RedirectURL = v @@ -401,7 +397,7 @@ func CallbackHandler(w http.ResponseWriter, r *http.Request) { if ok, err := VerifyUser(user); !ok { log.Error(err) - renderIndex(w, fmt.Sprintf("User is not authorized. %s Please try agian.", err)) + renderIndex(w, fmt.Sprintf("User is not authorized. %s Please try again.", err)) return } @@ -444,9 +440,10 @@ func getUserInfo(r *http.Request, user *structs.User) error { if err != nil { return err } + // make the "third leg" request back to google to exchange the token for the userinfo client := oauthclient.Client(oauth2.NoContext, providerToken) - if gcred.ClientID != "" { + if genOauth.Provider == "google" { return getUserInfoFromGoogle(client, user) } else if genOauth.Provider == "github" { return getUserInfoFromGithub(client, user, providerToken) diff --git a/pkg/structs/structs.go b/pkg/structs/structs.go index 5f98f491..43f957c0 100644 --- a/pkg/structs/structs.go +++ b/pkg/structs/structs.go @@ -31,15 +31,6 @@ type GithubUser struct { // jwt.StandardClaims } -// GCredentials google credentials -// loaded from yaml config -type GCredentials struct { - ClientID string `mapstructure:"client_id"` - ClientSecret string `mapstructure:"client_secret"` - RedirectURLs []string `mapstructure:"callback_urls"` - PreferredDomain string `mapstructre:"preferredDomain"` -} - // GenericOauth provides endoint for access type GenericOauth struct { ClientID string `mapstructure:"client_id"` @@ -47,9 +38,11 @@ type GenericOauth struct { AuthURL string `mapstructure:"auth_url"` TokenURL string `mapstructure:"token_url"` RedirectURL string `mapstructure:"callback_url"` + RedirectURLs []string `mapstructure:"callback_urls"` Scopes []string `mapstructure:"scopes"` UserInfoURL string `mapstructure:"user_info_url"` Provider string `mapstructure:"provider"` + PreferredDomain string `mapstructre:"preferredDomain"` } // Team has members and provides acess to sites From d860499e707477f4c919dd515c4ee9c52d5c38df Mon Sep 17 00:00:00 2001 From: Aaron Parecki Date: Fri, 17 Aug 2018 14:44:06 -0400 Subject: [PATCH 016/360] move to LassoProject org --- Dockerfile | 6 +++--- do.sh | 2 +- handlers/handlers.go | 14 +++++++------- main.go | 10 +++++----- pkg/cfg/cfg_test.go | 2 +- pkg/cookie/cookie.go | 6 +++--- pkg/domains/domains.go | 2 +- pkg/jwtmanager/jwtmanager.go | 4 ++-- pkg/jwtmanager/jwtmanager_test.go | 4 ++-- pkg/model/model.go | 2 +- pkg/model/model_test.go | 2 +- pkg/model/site.go | 2 +- pkg/model/team.go | 2 +- pkg/model/user.go | 2 +- pkg/timelog/timelog.go | 2 +- pkg/transciever/client.go | 4 ++-- 16 files changed, 33 insertions(+), 33 deletions(-) diff --git a/Dockerfile b/Dockerfile index e2f797e9..8664a808 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,9 @@ # bfoote/lasso -# https://github.com/bnfinet/lasso +# https://github.com/LassoProject/lasso FROM golang:1.8 -RUN mkdir -p ${GOPATH}/src/github.com/bnfinet/lasso -WORKDIR ${GOPATH}/src/github.com/bnfinet/lasso +RUN mkdir -p ${GOPATH}/src/github.com/LassoProject/lasso +WORKDIR ${GOPATH}/src/github.com/LassoProject/lasso COPY . . diff --git a/do.sh b/do.sh index 78afcf5d..1c552444 100755 --- a/do.sh +++ b/do.sh @@ -6,7 +6,7 @@ SCRIPT=$(readlink -f "$0") SDIR=$(dirname "$SCRIPT") cd $SDIR -export LASSO_ROOT=${GOPATH}/src/github.com/bnfinet/lasso/ +export LASSO_ROOT=${GOPATH}/src/github.com/LassoProject/lasso/ IMAGE=bfoote/lasso GOIMAGE=golang:1.8 diff --git a/handlers/handlers.go b/handlers/handlers.go index 018a9846..49fffee4 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -15,13 +15,13 @@ import ( log "github.com/Sirupsen/logrus" - "github.com/bnfinet/lasso/pkg/cfg" - lctx "github.com/bnfinet/lasso/pkg/context" - "github.com/bnfinet/lasso/pkg/cookie" - "github.com/bnfinet/lasso/pkg/domains" - "github.com/bnfinet/lasso/pkg/jwtmanager" - "github.com/bnfinet/lasso/pkg/model" - "github.com/bnfinet/lasso/pkg/structs" + "github.com/LassoProject/lasso/pkg/cfg" + lctx "github.com/LassoProject/lasso/pkg/context" + "github.com/LassoProject/lasso/pkg/cookie" + "github.com/LassoProject/lasso/pkg/domains" + "github.com/LassoProject/lasso/pkg/jwtmanager" + "github.com/LassoProject/lasso/pkg/model" + "github.com/LassoProject/lasso/pkg/structs" "github.com/gorilla/sessions" "golang.org/x/oauth2" "golang.org/x/oauth2/google" diff --git a/main.go b/main.go index 5cc3b906..764cb977 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,7 @@ package main // lasso -// github.com/bnfinet/lasso +// github.com/LassoProject/lasso import ( "net/http" @@ -10,10 +10,10 @@ import ( log "github.com/Sirupsen/logrus" - "github.com/bnfinet/lasso/handlers" - "github.com/bnfinet/lasso/pkg/cfg" - "github.com/bnfinet/lasso/pkg/timelog" - tran "github.com/bnfinet/lasso/pkg/transciever" + "github.com/LassoProject/lasso/handlers" + "github.com/LassoProject/lasso/pkg/cfg" + "github.com/LassoProject/lasso/pkg/timelog" + tran "github.com/LassoProject/lasso/pkg/transciever" ) func main() { diff --git a/pkg/cfg/cfg_test.go b/pkg/cfg/cfg_test.go index 86fb5ead..c8a21064 100644 --- a/pkg/cfg/cfg_test.go +++ b/pkg/cfg/cfg_test.go @@ -2,7 +2,7 @@ package cfg import ( "testing" - // "github.com/bnfinet/lasso/pkg/structs" + // "github.com/LassoProject/lasso/pkg/structs" // log "github.com/Sirupsen/logrus" log "github.com/Sirupsen/logrus" "github.com/stretchr/testify/assert" diff --git a/pkg/cookie/cookie.go b/pkg/cookie/cookie.go index e8c9baa5..a5ad22ee 100644 --- a/pkg/cookie/cookie.go +++ b/pkg/cookie/cookie.go @@ -4,9 +4,9 @@ import ( "errors" "net/http" - // "github.com/bnfinet/lasso/pkg/structs" - "github.com/bnfinet/lasso/pkg/cfg" - "github.com/bnfinet/lasso/pkg/domains" + // "github.com/LassoProject/lasso/pkg/structs" + "github.com/LassoProject/lasso/pkg/cfg" + "github.com/LassoProject/lasso/pkg/domains" log "github.com/Sirupsen/logrus" ) diff --git a/pkg/domains/domains.go b/pkg/domains/domains.go index ce6c8453..aec4bd59 100644 --- a/pkg/domains/domains.go +++ b/pkg/domains/domains.go @@ -3,7 +3,7 @@ package domains import ( "strings" - "github.com/bnfinet/lasso/pkg/cfg" + "github.com/LassoProject/lasso/pkg/cfg" log "github.com/Sirupsen/logrus" ) diff --git a/pkg/jwtmanager/jwtmanager.go b/pkg/jwtmanager/jwtmanager.go index d9d61e05..f15ad60e 100644 --- a/pkg/jwtmanager/jwtmanager.go +++ b/pkg/jwtmanager/jwtmanager.go @@ -11,8 +11,8 @@ import ( "time" log "github.com/Sirupsen/logrus" - "github.com/bnfinet/lasso/pkg/cfg" - "github.com/bnfinet/lasso/pkg/structs" + "github.com/LassoProject/lasso/pkg/cfg" + "github.com/LassoProject/lasso/pkg/structs" jwt "github.com/dgrijalva/jwt-go" ) diff --git a/pkg/jwtmanager/jwtmanager_test.go b/pkg/jwtmanager/jwtmanager_test.go index ed08027a..b9211cd7 100644 --- a/pkg/jwtmanager/jwtmanager_test.go +++ b/pkg/jwtmanager/jwtmanager_test.go @@ -3,8 +3,8 @@ package jwtmanager import ( "testing" - "github.com/bnfinet/lasso/pkg/cfg" - "github.com/bnfinet/lasso/pkg/structs" + "github.com/LassoProject/lasso/pkg/cfg" + "github.com/LassoProject/lasso/pkg/structs" // log "github.com/Sirupsen/logrus" log "github.com/Sirupsen/logrus" "github.com/stretchr/testify/assert" diff --git a/pkg/model/model.go b/pkg/model/model.go index 83b74110..6dd8e2b4 100644 --- a/pkg/model/model.go +++ b/pkg/model/model.go @@ -8,7 +8,7 @@ import ( "os" "time" - "github.com/bnfinet/lasso/pkg/cfg" + "github.com/LassoProject/lasso/pkg/cfg" log "github.com/Sirupsen/logrus" "github.com/boltdb/bolt" ) diff --git a/pkg/model/model_test.go b/pkg/model/model_test.go index 663ffa4a..a56a382b 100644 --- a/pkg/model/model_test.go +++ b/pkg/model/model_test.go @@ -11,7 +11,7 @@ import ( "github.com/stretchr/testify/assert" - "github.com/bnfinet/lasso/pkg/structs" + "github.com/LassoProject/lasso/pkg/structs" ) var testdb = "/tmp/storage-test.db" diff --git a/pkg/model/site.go b/pkg/model/site.go index 9b43da6c..3b149924 100644 --- a/pkg/model/site.go +++ b/pkg/model/site.go @@ -6,7 +6,7 @@ import ( "time" log "github.com/Sirupsen/logrus" - "github.com/bnfinet/lasso/pkg/structs" + "github.com/LassoProject/lasso/pkg/structs" "github.com/boltdb/bolt" ) diff --git a/pkg/model/team.go b/pkg/model/team.go index 1a378633..eccbd93c 100644 --- a/pkg/model/team.go +++ b/pkg/model/team.go @@ -7,7 +7,7 @@ import ( "time" log "github.com/Sirupsen/logrus" - "github.com/bnfinet/lasso/pkg/structs" + "github.com/LassoProject/lasso/pkg/structs" "github.com/boltdb/bolt" ) diff --git a/pkg/model/user.go b/pkg/model/user.go index 3ed3730f..06897c22 100644 --- a/pkg/model/user.go +++ b/pkg/model/user.go @@ -7,7 +7,7 @@ import ( "time" log "github.com/Sirupsen/logrus" - "github.com/bnfinet/lasso/pkg/structs" + "github.com/LassoProject/lasso/pkg/structs" "github.com/boltdb/bolt" ) diff --git a/pkg/timelog/timelog.go b/pkg/timelog/timelog.go index 65ce51dd..8c149b65 100644 --- a/pkg/timelog/timelog.go +++ b/pkg/timelog/timelog.go @@ -5,7 +5,7 @@ import ( "net/http" "time" - lctx "github.com/bnfinet/lasso/pkg/context" + lctx "github.com/LassoProject/lasso/pkg/context" log "github.com/Sirupsen/logrus" // "github.com/mattn/go-isatty" diff --git a/pkg/transciever/client.go b/pkg/transciever/client.go index 944ed28b..dd396a43 100644 --- a/pkg/transciever/client.go +++ b/pkg/transciever/client.go @@ -6,8 +6,8 @@ import ( "net/http" "time" - "github.com/bnfinet/lasso/pkg/model" - "github.com/bnfinet/lasso/pkg/structs" + "github.com/LassoProject/lasso/pkg/model" + "github.com/LassoProject/lasso/pkg/structs" log "github.com/Sirupsen/logrus" "github.com/mitchellh/mapstructure" From 06a05a36e1fc52021196fe044149b5f58e8eea8b Mon Sep 17 00:00:00 2001 From: Aaron Parecki Date: Fri, 17 Aug 2018 14:45:10 -0400 Subject: [PATCH 017/360] add empty data folder --- data/.gitignore | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 data/.gitignore diff --git a/data/.gitignore b/data/.gitignore new file mode 100644 index 00000000..e69de29b From e988d045d21342bad0097ecd1be0b56460262e7e Mon Sep 17 00:00:00 2001 From: Aaron Parecki Date: Fri, 17 Aug 2018 14:49:03 -0400 Subject: [PATCH 018/360] update config example for readability --- config/config.yml_example | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/config/config.yml_example b/config/config.yml_example index 5611abf4..013af8ce 100644 --- a/config/config.yml_example +++ b/config/config.yml_example @@ -4,23 +4,28 @@ lasso: logLevel: info listen: 0.0.0.0 port: 9090 + # set allowAllUsers: true to use Lasso to just accept anyone who can authenticate at the configured provider allowAllUsers: false + # Setting publicAccess: true will accept all requests, even without a cookie. # If the user is logged in, the cookie will be validated and the user header will be set. # You will need to direct people to the Lasso login page from your application. publicAccess: false + # each of these domains must serve the url https://lasso.$domains[0] https://lasso.$domains[1] ... # usually you'll just have one. # Comment this out if you set allowAllUser:true domains: - yourdomain.com - yourotherdomain.com + jwt: issuer: Lasso maxAge: 240 secret: your_random_string compress: true + cookie: # name of cookie to store the jwt name: Lasso-cookie @@ -28,14 +33,18 @@ lasso: # domain: yourdomain.com secure: true httpOnly: true + + session: + name: lasso-session + headers: jwt: X-Lasso-Token querystring: access_token redirect: X-Lasso-Requested-URI + db: file: data/lasso_bolt.db - session: - name: lasso-session + test_url: http://yourdomain.com # @@ -50,7 +59,6 @@ oauth: # https://console.developers.google.com/apis/credentials client_id: client_secret: - # must be the /auth endpoint callback_urls: - http://lasso.yourdomain.com:9090/auth - http://lasso.yourotherdomain.com:9090/auth @@ -64,6 +72,7 @@ oauth: auth_url: https://github.com/login/oauth/authorize token_url: https://github.com/login/oauth/access_token # callback_url is configured at github.com when setting up the app + # set to e.g. https://lasso.yourdomain.com/auth scopes: - user user_info_url: https://api.github.com/user?access_token= From 87929d407d97efe3405f8fc4ba51d0516272ca4f Mon Sep 17 00:00:00 2001 From: Qingsong Yao Date: Fri, 21 Sep 2018 14:07:53 -0700 Subject: [PATCH 019/360] use genOauth.UserInfoURL if it is set --- handlers/handlers.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/handlers/handlers.go b/handlers/handlers.go index 49fffee4..1a65831a 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -496,7 +496,11 @@ func getUserInfoFromGoogle(client *http.Client, user *structs.User) error { func getUserInfoFromGithub(client *http.Client, user *structs.User, ptoken *oauth2.Token) error { log.Errorf("ptoken.AccessToken: %s", ptoken.AccessToken) - userinfo, err := client.Get("https://api.github.com/user?access_token=" + ptoken.AccessToken) + userInfoUrl := "https://api.github.com/user?access_token=" + if genOauth.UserInfoURL != "" { + userInfoUrl = genOauth.UserInfoURL + } + userinfo, err := client.Get(userInfoUrl + ptoken.AccessToken) if err != nil { // http.Error(w, err.Error(), http.StatusBadRequest) return err From 1fe3ac34d53037133deb85ac75253c965a28aff1 Mon Sep 17 00:00:00 2001 From: Benjamin Foote Date: Mon, 24 Sep 2018 13:10:49 -0700 Subject: [PATCH 020/360] aaron indieauth TODOs --- TODO.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/TODO.md b/TODO.md index 579f89b8..c04a1471 100644 --- a/TODO.md +++ b/TODO.md @@ -5,15 +5,43 @@ ## TODO +* aaronpk 2017-10-04 + ‎[15:46] ‎<‎aaronpk‎>‎ so, immediate feature request is to be able to whitelist specific email addresses instead of doing domain matching for users + + +* aaronpk + ‎[16:40] ‎<‎aaronpk‎>‎ sure! basically i want to redirect to https://indieauth.com instead of google auth + ‎[16:41] ‎<‎aaronpk‎>‎ and there's an endpoint there that the plugin can use to verify the auth code and get user info + ‎[16:44] ‎<‎aaronpk‎>‎ so being able to customize this URL or maybe even override some method to be ableto customize the handling of the verification https://github.com/bnfinet/lasso/blob/master/handlers/handlers.go#L313 + ‎[16:49] ‎<‎aaronpk‎>‎ here's the docs i was walking you through https://indieauth.com/developers + ‎[16:53] ‎<‎bfoote‎>‎ oh that's looks pretty straight forward + +* add config for oauth Enpoint + https://github.com/golang/oauth2/blob/master/github/github.go + if endpoing is ~= google then allow 'hd' and accomodate getting User info + * is user info for Oauth a standard form? Probably _no_. Going to need some interpreters. + * create a special team for admins * look for the token in an "Authorization: bearer $TOKEN" header +* include static assets in binary + https://github.com/shurcooL/vfsgen + * restapi * `/api/validate` endpoint that *any* service can connect to that validates the `X-LASSO-TOKEN` header * add lastupdate to user, sites, team +* handle multiple domains + * set the `Oauth2.config{RedirectURL}` Google callback URL dynamically based on the domain that was offered + + +* iterate through a list of authorized domains + * 302 redirect to the next domain + * set a jwt cookie into each domain + * might slow down login + * how to handle "not authorized for domain"? * can nginx pass a 302 back to /login with an argument in the querystring such as.. /login?jwt=$COOKIE @@ -60,6 +88,9 @@ ## DONE +* set X-Lasso-User header passed through to the backend app + https://stackoverflow.com/questions/19366215/setting-headers-with-nginx-auth-request-proxy#19366411 + * replace gin.Cookie with gorilla.cookie * optionally compress the cookie (gzip && base64) From 9653df6ccaf4c9887c54a11f9d69a701fbf7ba70 Mon Sep 17 00:00:00 2001 From: Benjamin Foote Date: Mon, 24 Sep 2018 13:14:26 -0700 Subject: [PATCH 021/360] minor case and syntax changes --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 414a2e1e..3aaee2a1 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Lasso -an SSO solution for an nginx reverse proxy using the [auth_request](http://nginx.org/en/docs/http/ngx_http_auth_request_module.html) module +an SSO solution for nginx using the [auth_request](http://nginx.org/en/docs/http/ngx_http_auth_request_module.html) module -lasso supports oauth for google apps, [github](https://developer.github.com/apps/building-integrations/setting-up-and-registering-oauth-apps/about-authorization-options-for-oauth-apps/) and [indieauth](https://indieauth.com/developers) +lasso supports OAuth login to google apps, [github](https://developer.github.com/apps/building-integrations/setting-up-and-registering-oauth-apps/about-authorization-options-for-oauth-apps/) and [indieauth](https://indieauth.com/developers) If lasso is running on the same host as the nginx reverse proxy the response time from the `/validate` endpoint to nginx should be less than 1ms @@ -82,7 +82,8 @@ And that's it! Or if you can examine the docker command in `do.sh` The [bfoote/lasso](https://hub.docker.com/r/bfoote/lasso/) Docker image is an automated build on Docker Hub ## Running from source -``` + +```bash go get ./... go build ./lasso @@ -106,7 +107,7 @@ The [bfoote/lasso](https://hub.docker.com/r/bfoote/lasso/) Docker image is an au * if the cookie is found, and the JWT is valid * returns 200 to nginx, which will allow access (bob notices nothing) * if the cookie is NOT found, or the JWT is NOT valid - * return 401 NotAuthorized to nginx (which forwards the request on to login) + * return 401 NotAuthorized to nginx (which forwards the request on to login) * Bob is first forwarded briefly to `https://lasso.oursites.com/login?url=https://private.oursites.com` * clears out the cookie named "oursitesSSO" if it exists From 4264930a5a28fab64e2272c3aa3bb4895ac295a3 Mon Sep 17 00:00:00 2001 From: Benjamin Foote Date: Mon, 24 Sep 2018 13:18:20 -0700 Subject: [PATCH 022/360] delete team --- pkg/model/model_test.go | 11 ++++++++++- pkg/model/team.go | 20 +++++++++++++++++--- pkg/transciever/client.go | 17 +++++++++++++++++ 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/pkg/model/model_test.go b/pkg/model/model_test.go index 663ffa4a..67bd3402 100644 --- a/pkg/model/model_test.go +++ b/pkg/model/model_test.go @@ -71,12 +71,13 @@ func TestPutSiteGetSite(t *testing.T) { assert.Equal(t, s1.Domain, s2.Domain) } -func TestPutTeamGetTeam(t *testing.T) { +func TestPutTeamGetTeamDeleteTeam(t *testing.T) { os.Remove(testdb) Open(testdb) t1 := structs.Team{Name: "testname"} t2 := &structs.Team{} + t3 := &structs.Team{} if err := PutTeam(t1); err != nil { log.Error(err) @@ -84,4 +85,12 @@ func TestPutTeamGetTeam(t *testing.T) { Team([]byte(t1.Name), t2) log.Debugf("team retrieved: %v", *t2) assert.Equal(t, t1.Name, t2.Name) + + if err := DeleteTeam(t1); err != nil { + log.Error(err) + } + // should fail + err := Team([]byte(t1.Name), t3) + assert.Error(t, err) + } diff --git a/pkg/model/team.go b/pkg/model/team.go index 1a378633..b69851c6 100644 --- a/pkg/model/team.go +++ b/pkg/model/team.go @@ -11,10 +11,10 @@ import ( "github.com/boltdb/bolt" ) -// PutTeam inna da db +// PutTeam - create or update a team func PutTeam(t structs.Team) error { teamexists := false - curt := &structs.Team{} + curt := &structs.Team{} // curt == current team err := Team([]byte(t.Name), curt) if err == nil { teamexists = true @@ -26,7 +26,7 @@ func PutTeam(t structs.Team) error { if b := getBucket(tx, teamBucket); b != nil { t.LastUpdate = time.Now().Unix() if teamexists { - log.Debugf("teamexists.. keeping time at %v", curt.CreatedOn) + log.Debugf("teamexists.. keeping time at %v, members are %v", curt.CreatedOn, curt.Members) t.CreatedOn = curt.CreatedOn } else { id, _ := b.NextSequence() @@ -66,6 +66,20 @@ func Team(key []byte, t *structs.Team) error { }) } +// DeleteTeam from key +func DeleteTeam(t structs.Team) error { + return Db.Update(func(tx *bolt.Tx) error { + if b := tx.Bucket(teamBucket); b != nil { + if err := b.Delete([]byte(t.Name)); err != nil { + return err + } + log.Debugf("deleted %s from db", t.Name) + return nil + } + return fmt.Errorf("no bucket for %s", teamBucket) + }) +} + // AllTeams collect all items func AllTeams(teams *[]structs.Team) error { return Db.View(func(tx *bolt.Tx) error { diff --git a/pkg/transciever/client.go b/pkg/transciever/client.go index 944ed28b..fb4a8d55 100644 --- a/pkg/transciever/client.go +++ b/pkg/transciever/client.go @@ -100,6 +100,8 @@ func (c *Client) readPump() { c.shipTeams() } else if p.T == "updateteam" { c.updateTeam(p.D) + } else if p.T == "deleteteam" { + c.deleteTeam(p.D) } // c.hub.broadcast <- []byte(p) } @@ -118,6 +120,21 @@ func (c *Client) updateTeam(data interface{}) { c.shipTeams() } +func (c *Client) deleteTeam(data interface{}) { + + t := structs.Team{} + mapstructure.Decode(data, &t) + log.Debugf("deleting team %v", t) + model.DeleteTeam(t) + testT := structs.Team{} + if err := model.Team([]byte(t.Name), &testT); err != nil { + log.Error(err) + } + log.Debugf("if deleted should be null: %s", testT.Name) + + c.shipTeams() +} + // writePump pumps messages from the hub to the websocket connection. // // A goroutine running writePump is started for each connection. The From 60e6a79b95c05eaee7cae6d719316d4951b34d23 Mon Sep 17 00:00:00 2001 From: Benjamin Foote Date: Mon, 24 Sep 2018 13:19:29 -0700 Subject: [PATCH 023/360] AUTHORS initial commit --- AUTHORS.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 AUTHORS.txt diff --git a/AUTHORS.txt b/AUTHORS.txt new file mode 100644 index 00000000..8f5d90ad --- /dev/null +++ b/AUTHORS.txt @@ -0,0 +1,2 @@ +bnfinet +aaronpk \ No newline at end of file From b874b9ca86890ecfb295261d1bcb1791f9560bbe Mon Sep 17 00:00:00 2001 From: Benjamin Foote Date: Sun, 30 Sep 2018 16:21:00 -0700 Subject: [PATCH 024/360] make note about $auth_resp_x_lasso_user in `location /` as per #26 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3aaee2a1..52fe57ed 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ server { # send all requests to the `/validate` endpoint for authorization auth_request /validate; - # if validate returns `401 not authorized` then forward the request to the error401block + # if validate returns `401 not authorized` then forward the request to the error401block error_page 401 = @error401; location = /validate { From bbb1dccddc32014a62ecb15185dbae1323fbe276 Mon Sep 17 00:00:00 2001 From: Benjamin Foote Date: Tue, 2 Oct 2018 11:17:23 -0700 Subject: [PATCH 025/360] add cookie explanation --- config/config.yml_example | 1 + 1 file changed, 1 insertion(+) diff --git a/config/config.yml_example b/config/config.yml_example index 013af8ce..06dddcb9 100644 --- a/config/config.yml_example +++ b/config/config.yml_example @@ -14,6 +14,7 @@ lasso: publicAccess: false # each of these domains must serve the url https://lasso.$domains[0] https://lasso.$domains[1] ... + # so that the cookie which stores the JWT can be set in the relevant domain # usually you'll just have one. # Comment this out if you set allowAllUser:true domains: From bbe437b5d7128cf50185eb0bb85c7c24b359dc28 Mon Sep 17 00:00:00 2001 From: Benjamin Foote Date: Tue, 2 Oct 2018 12:34:28 -0700 Subject: [PATCH 026/360] minor formatting changes --- handlers/handlers.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/handlers/handlers.go b/handlers/handlers.go index 1a65831a..63f4f61c 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -234,6 +234,7 @@ func ValidateRequestHandler(w http.ResponseWriter, r *http.Request) { // renderIndex(w, "user found from email "+user.Email) w.Header().Add("X-Lasso-User", claims.Email) + w.Header().Add("X-Lasso-Success", "true"); log.Debugf("X-Lasso-User response headers %s", w.Header().Get("X-Lasso-User")) renderIndex(w, "user found in jwt "+claims.Email) @@ -449,9 +450,8 @@ func getUserInfo(r *http.Request, user *structs.User) error { return getUserInfoFromGithub(client, user, providerToken) } else if genOauth.Provider == "oidc" { return getUserInfoFromOpenID(client, user, providerToken) - } else { - log.Error("we don't know how to look up the user info") } + log.Error("we don't know how to look up the user info") return nil } @@ -496,11 +496,11 @@ func getUserInfoFromGoogle(client *http.Client, user *structs.User) error { func getUserInfoFromGithub(client *http.Client, user *structs.User, ptoken *oauth2.Token) error { log.Errorf("ptoken.AccessToken: %s", ptoken.AccessToken) - userInfoUrl := "https://api.github.com/user?access_token=" + userInfoURL := "https://api.github.com/user?access_token=" if genOauth.UserInfoURL != "" { - userInfoUrl = genOauth.UserInfoURL + userInfoURL = genOauth.UserInfoURL } - userinfo, err := client.Get(userInfoUrl + ptoken.AccessToken) + userinfo, err := client.Get(userInfoURL + ptoken.AccessToken) if err != nil { // http.Error(w, err.Error(), http.StatusBadRequest) return err From 22299b98647515cc1a198c466e79d5cb37de1d1f Mon Sep 17 00:00:00 2001 From: Benjamin Foote Date: Tue, 2 Oct 2018 13:00:30 -0700 Subject: [PATCH 027/360] don't store lasso binary --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 5941f189..02aec4ee 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ data/lasso_bolt.db pkg/model/storage-test.db main config/google_config.json -.vscode/* \ No newline at end of file +.vscode/* +lasso From 85762493ad0ffbae37d13164727120016d0cd264 Mon Sep 17 00:00:00 2001 From: Benjamin Foote Date: Tue, 2 Oct 2018 13:30:03 -0700 Subject: [PATCH 028/360] set vars in / block as per #26 --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 52fe57ed..96d9e4be 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,9 @@ server { # proxy pass authorized requests to your service location / { proxy_pass http://dev.yourdomain.com:8080; + # may need to set + # auth_request_set $auth_resp_x_lasso_user $upstream_http_x_lasso_user + # in this bock as per https://github.com/LassoProject/lasso/issues/26#issuecomment-425215810 # set user header (usually an email) proxy_set_header X-Lasso-User $auth_resp_x_lasso_user; } From ad0ee41b489a1bbd2100997746f1eb4f8f5e4c31 Mon Sep 17 00:00:00 2001 From: Benjamin Foote Date: Tue, 2 Oct 2018 13:33:56 -0700 Subject: [PATCH 029/360] fix #6 log error when URL is not set and render index --- handlers/handlers.go | 42 ++++++++++++++++++++++++------------------ templates/index.tmpl | 5 +++++ 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/handlers/handlers.go b/handlers/handlers.go index 63f4f61c..c5792288 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -277,9 +277,10 @@ func LogoutHandler(w http.ResponseWriter, r *http.Request) { session.Save(r, w) sessstore.MaxAge(300) - var redirectURL = r.URL.Query().Get("url") - if redirectURL != "" { - http.Redirect(w, r, redirectURL, 302); + + var requestedURL = r.URL.Query().Get("url") + if requestedURL != "" { + http.Redirect(w, r, requestedURL, 302); } else { renderIndex(w, "you have been logged out") } @@ -297,36 +298,41 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) { log.Error(err) } - // set the state varialbe in the session + // set the state variable in the session var state = randString() session.Values["state"] = state log.Debugf("session state set to %s", session.Values["state"]) // increment the failure counter for this domain - // redirectURL comes from nginx in the query string - var redirectURL = r.URL.Query().Get("url") - if redirectURL != "" { - // TODO store the originally requested URL so we can redirec on the roundtrip - session.Values["requestedURL"] = redirectURL + // requestedURL comes from nginx in the query string via a 302 redirect + // it sets the ultimate destination + // https://lasso.yoursite.com/login?url= + var requestedURL = r.URL.Query().Get("url"); + if (requestedURL == "") { + renderIndex(w, "no destination URL requested") + log.Error("no destination URL requested") + return + } else { + session.Values["requestedURL"] = requestedURL log.Debugf("session requestedURL set to %s", session.Values["requestedURL"]) } // stop them after three failures for this URL var failcount = 0 - if session.Values[redirectURL] != nil { - failcount = session.Values[redirectURL].(int) - log.Debugf("failcount for %s is %d", redirectURL, failcount) + if session.Values[requestedURL] != nil { + failcount = session.Values[requestedURL].(int) + log.Debugf("failcount for %s is %d", requestedURL, failcount) } failcount++ - session.Values[redirectURL] = failcount + session.Values[requestedURL] = failcount log.Debug("saving session") session.Save(r, w) if failcount > 2 { var lassoError = r.URL.Query().Get("error") - renderIndex(w, "too many redirects for "+redirectURL+" - "+lassoError) + renderIndex(w, "too many redirects for "+requestedURL+" - "+lassoError) } else { // bounce to oauth provider for login var lURL = loginURL(r, state) @@ -412,16 +418,16 @@ func CallbackHandler(w http.ResponseWriter, r *http.Request) { cookie.SetCookie(w, r, tokenstring) // get the originally requested URL so we can send them on their way - redirectURL := session.Values["requestedURL"].(string) - if redirectURL != "" { + requestedURL := session.Values["requestedURL"].(string) + if requestedURL != "" { // clear out the session value session.Values["requestedURL"] = "" - session.Values[redirectURL] = 0 + session.Values[requestedURL] = 0 session.Save(r, w) // and redirect context.WithValue(r.Context(), lctx.StatusCode, 302) - http.Redirect(w, r, redirectURL, 302) + http.Redirect(w, r, requestedURL, 302) return } // otherwise serve an html page diff --git a/templates/index.tmpl b/templates/index.tmpl index 12502c6d..ecfcf43b 100644 --- a/templates/index.tmpl +++ b/templates/index.tmpl @@ -3,6 +3,7 @@ + Lasso: {{ .Msg }}. @@ -15,5 +16,9 @@
  • {{ .TestURL }}
  • +For support, please contact your network administrator or whomever setup nginx to use Lasso. +

    +For help with Lasso or to file a bug report, please see the project page at https://github.com/LassoProject/lasso +

    From 65b7438fc86e64e2d3ac5ddb74b0cce1b4dfbd98 Mon Sep 17 00:00:00 2001 From: Benjamin Foote Date: Tue, 2 Oct 2018 14:34:31 -0700 Subject: [PATCH 030/360] fix #13 test for required config options, and panic if they're not there --- pkg/cfg/cfg.go | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/pkg/cfg/cfg.go b/pkg/cfg/cfg.go index 41b90e24..f1b8adec 100644 --- a/pkg/cfg/cfg.go +++ b/pkg/cfg/cfg.go @@ -1,6 +1,7 @@ package cfg import ( + "errors" "flag" "os" @@ -46,8 +47,8 @@ type CfgT struct { // Cfg the main exported config variable var Cfg CfgT -// V viper object -// var V viper +// RequiredOptions must have these fields set for minimum viable config +var RequiredOptions = []string{"lasso.port", "lasso.listen", "lasso.domains", "lasso.jwt.secret", "lasso.db.file", "oauth.provider", "oauth.client_id", "oauth.client_secret"} func init() { ParseConfig() @@ -72,6 +73,11 @@ func ParseConfig() { panic(err) } UnmarshalKey("lasso", &Cfg) + errT := BasicTest() + if errT != nil { + // log.Fatalf(err.prob) + panic(errT) + } // nested defaults is currently *broken* // https://github.com/spf13/viper/issues/309 // viper.SetDefault("listen", "0.0.0.0") @@ -91,3 +97,13 @@ func UnmarshalKey(key string, rawVal interface{}) error { func Get(key string) string { return viper.GetString(key) } + +// BasicTest just a quick sanity check to see if the config is sound +func BasicTest() error { + for _, opt := range RequiredOptions { + if (!viper.IsSet(opt)) { + return errors.New("configuration option " + opt + " is not set in config") + } + } + return nil +} \ No newline at end of file From ca6dce74af233f3ea260b6c6f904c2d63103eb1b Mon Sep 17 00:00:00 2001 From: Benjamin Foote Date: Tue, 2 Oct 2018 15:39:56 -0700 Subject: [PATCH 031/360] don't require lasso.domains --- pkg/cfg/cfg.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/cfg/cfg.go b/pkg/cfg/cfg.go index f1b8adec..44c23480 100644 --- a/pkg/cfg/cfg.go +++ b/pkg/cfg/cfg.go @@ -31,9 +31,9 @@ type CfgT struct { HTTPOnly bool `mapstructure:"httpOnly"` } Headers struct { - JWT string `mapstructure:"jwt"` + JWT string `mapstructure:"jwt"` QueryString string `mapstructure:"querystring"` - Redirect string `mapstructure:"redirect"` + Redirect string `mapstructure:"redirect"` } DB struct { File string `mapstructure:"file"` @@ -48,7 +48,7 @@ type CfgT struct { var Cfg CfgT // RequiredOptions must have these fields set for minimum viable config -var RequiredOptions = []string{"lasso.port", "lasso.listen", "lasso.domains", "lasso.jwt.secret", "lasso.db.file", "oauth.provider", "oauth.client_id", "oauth.client_secret"} +var RequiredOptions = []string{"lasso.port", "lasso.listen", "lasso.jwt.secret", "lasso.db.file", "oauth.provider", "oauth.client_id", "oauth.client_secret"} func init() { ParseConfig() @@ -101,9 +101,9 @@ func Get(key string) string { // BasicTest just a quick sanity check to see if the config is sound func BasicTest() error { for _, opt := range RequiredOptions { - if (!viper.IsSet(opt)) { + if !viper.IsSet(opt) { return errors.New("configuration option " + opt + " is not set in config") } } return nil -} \ No newline at end of file +} From 98d5fbc6e91c63c7b9e3fb2a47a9ac12f91e1694 Mon Sep 17 00:00:00 2001 From: Benjamin Foote Date: Tue, 2 Oct 2018 16:25:12 -0700 Subject: [PATCH 032/360] use ghUser and set User.email from ghUser.login --- handlers/handlers.go | 34 ++++++++++++++++++++++------------ pkg/structs/structs.go | 20 +++++++++++--------- 2 files changed, 33 insertions(+), 21 deletions(-) diff --git a/handlers/handlers.go b/handlers/handlers.go index c5792288..26a89159 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -195,7 +195,7 @@ func ValidateRequestHandler(w http.ResponseWriter, r *http.Request) { if !cfg.Cfg.PublicAccess { error401(w, r, AuthError{Error: "no jwt found"}) } else { - w.Header().Add("X-Lasso-User", ""); + w.Header().Add("X-Lasso-User", "") } return } @@ -206,7 +206,7 @@ func ValidateRequestHandler(w http.ResponseWriter, r *http.Request) { if !cfg.Cfg.PublicAccess { error401(w, r, AuthError{err.Error(), jwt}) } else { - w.Header().Add("X-Lasso-User", ""); + w.Header().Add("X-Lasso-User", "") } return } @@ -215,7 +215,7 @@ func ValidateRequestHandler(w http.ResponseWriter, r *http.Request) { if !cfg.Cfg.PublicAccess { error401(w, r, AuthError{"no email found in jwt", jwt}) } else { - w.Header().Add("X-Lasso-User", ""); + w.Header().Add("X-Lasso-User", "") } return } @@ -226,7 +226,7 @@ func ValidateRequestHandler(w http.ResponseWriter, r *http.Request) { if !cfg.Cfg.PublicAccess { error401(w, r, AuthError{"not authorized for " + r.Host, jwt}) } else { - w.Header().Add("X-Lasso-User", ""); + w.Header().Add("X-Lasso-User", "") } return } @@ -234,7 +234,7 @@ func ValidateRequestHandler(w http.ResponseWriter, r *http.Request) { // renderIndex(w, "user found from email "+user.Email) w.Header().Add("X-Lasso-User", claims.Email) - w.Header().Add("X-Lasso-Success", "true"); + w.Header().Add("X-Lasso-Success", "true") log.Debugf("X-Lasso-User response headers %s", w.Header().Get("X-Lasso-User")) renderIndex(w, "user found in jwt "+claims.Email) @@ -277,10 +277,9 @@ func LogoutHandler(w http.ResponseWriter, r *http.Request) { session.Save(r, w) sessstore.MaxAge(300) - var requestedURL = r.URL.Query().Get("url") if requestedURL != "" { - http.Redirect(w, r, requestedURL, 302); + http.Redirect(w, r, requestedURL, 302) } else { renderIndex(w, "you have been logged out") } @@ -308,8 +307,8 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) { // requestedURL comes from nginx in the query string via a 302 redirect // it sets the ultimate destination // https://lasso.yoursite.com/login?url= - var requestedURL = r.URL.Query().Get("url"); - if (requestedURL == "") { + var requestedURL = r.URL.Query().Get("url") + if requestedURL == "" { renderIndex(w, "no destination URL requested") log.Error("no destination URL requested") return @@ -359,7 +358,10 @@ func VerifyUser(u interface{}) (ok bool, err error) { // TODO: how do we manage the user? user := u.(structs.User) - if len(cfg.Cfg.Domains) != 0 && !domains.IsUnderManagement(user.Email) { + if cfg.Cfg.AllowAllUsers { + ok = true + // if we're not allowing all users, and we have domains configured and this email isn't in one of those domains... + } else if len(cfg.Cfg.Domains) != 0 && !domains.IsUnderManagement(user.Email) { err = fmt.Errorf("Email %s is not within a lasso managed domain", user.Email) // } else if !domains.IsUnderManagement(user.HostDomain) { // err = fmt.Errorf("HostDomain %s is not within a lasso managed domain", u.HostDomain) @@ -371,7 +373,7 @@ func VerifyUser(u interface{}) (ok bool, err error) { } // CallbackHandler /auth -// - validate info from Google +// - validate info from oauth provider (Google, Github, OIDC, etc) // - create user // - issue jwt in the form of a cookie func CallbackHandler(w http.ResponseWriter, r *http.Request) { @@ -514,12 +516,20 @@ func getUserInfoFromGithub(client *http.Client, user *structs.User, ptoken *oaut defer userinfo.Body.Close() data, _ := ioutil.ReadAll(userinfo.Body) log.Println("github userinfo body: ", string(data)) - if err = json.Unmarshal(data, user); err != nil { + ghUser := &structs.GithubUser{} + if err = json.Unmarshal(data, ghUser); err != nil { + // if err = json.Unmarshal(data, user); err != nil { log.Errorln(err) // renderIndex(w, "Error marshalling response. Please try agian.") // c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{"message": }) return err } + ghUser.Email = ghUser.Login + if ghUser.Email == "" && ghUser.Login != "" { + log.Debug("no email returned from github, setting user email to login " + ghUser.Login) + } + user = &ghUser.User + log.Debug("getUserInfoFromGithub") log.Debug(user) return nil } diff --git a/pkg/structs/structs.go b/pkg/structs/structs.go index 43f957c0..1f6856ae 100644 --- a/pkg/structs/structs.go +++ b/pkg/structs/structs.go @@ -11,6 +11,7 @@ type User struct { } // GoogleUser is a retrieved and authentiacted user from Google. +// unused! type GoogleUser struct { User Sub string `json:"sub"` @@ -27,22 +28,23 @@ type GoogleUser struct { // GithubUser is a retrieved and authentiacted user from Github. type GithubUser struct { User + Login string `json:"login"` Picture string `json:"avatar_url"` // jwt.StandardClaims } // GenericOauth provides endoint for access type GenericOauth struct { - ClientID string `mapstructure:"client_id"` - ClientSecret string `mapstructure:"client_secret"` - AuthURL string `mapstructure:"auth_url"` - TokenURL string `mapstructure:"token_url"` - RedirectURL string `mapstructure:"callback_url"` + ClientID string `mapstructure:"client_id"` + ClientSecret string `mapstructure:"client_secret"` + AuthURL string `mapstructure:"auth_url"` + TokenURL string `mapstructure:"token_url"` + RedirectURL string `mapstructure:"callback_url"` RedirectURLs []string `mapstructure:"callback_urls"` - Scopes []string `mapstructure:"scopes"` - UserInfoURL string `mapstructure:"user_info_url"` - Provider string `mapstructure:"provider"` - PreferredDomain string `mapstructre:"preferredDomain"` + Scopes []string `mapstructure:"scopes"` + UserInfoURL string `mapstructure:"user_info_url"` + Provider string `mapstructure:"provider"` + PreferredDomain string `mapstructre:"preferredDomain"` } // Team has members and provides acess to sites From c5cec141e368acf47acf9531e8545201f95243fb Mon Sep 17 00:00:00 2001 From: Benjamin Foote Date: Wed, 3 Oct 2018 12:12:56 -0700 Subject: [PATCH 033/360] if testing capture 302 redirects --- handlers/handlers.go | 30 +++++++++++++++++++----------- pkg/cfg/cfg.go | 1 + 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/handlers/handlers.go b/handlers/handlers.go index c5792288..27cf6148 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -195,7 +195,7 @@ func ValidateRequestHandler(w http.ResponseWriter, r *http.Request) { if !cfg.Cfg.PublicAccess { error401(w, r, AuthError{Error: "no jwt found"}) } else { - w.Header().Add("X-Lasso-User", ""); + w.Header().Add("X-Lasso-User", "") } return } @@ -206,7 +206,7 @@ func ValidateRequestHandler(w http.ResponseWriter, r *http.Request) { if !cfg.Cfg.PublicAccess { error401(w, r, AuthError{err.Error(), jwt}) } else { - w.Header().Add("X-Lasso-User", ""); + w.Header().Add("X-Lasso-User", "") } return } @@ -215,7 +215,7 @@ func ValidateRequestHandler(w http.ResponseWriter, r *http.Request) { if !cfg.Cfg.PublicAccess { error401(w, r, AuthError{"no email found in jwt", jwt}) } else { - w.Header().Add("X-Lasso-User", ""); + w.Header().Add("X-Lasso-User", "") } return } @@ -226,7 +226,7 @@ func ValidateRequestHandler(w http.ResponseWriter, r *http.Request) { if !cfg.Cfg.PublicAccess { error401(w, r, AuthError{"not authorized for " + r.Host, jwt}) } else { - w.Header().Add("X-Lasso-User", ""); + w.Header().Add("X-Lasso-User", "") } return } @@ -234,7 +234,7 @@ func ValidateRequestHandler(w http.ResponseWriter, r *http.Request) { // renderIndex(w, "user found from email "+user.Email) w.Header().Add("X-Lasso-User", claims.Email) - w.Header().Add("X-Lasso-Success", "true"); + w.Header().Add("X-Lasso-Success", "true") log.Debugf("X-Lasso-User response headers %s", w.Header().Get("X-Lasso-User")) renderIndex(w, "user found in jwt "+claims.Email) @@ -277,10 +277,9 @@ func LogoutHandler(w http.ResponseWriter, r *http.Request) { session.Save(r, w) sessstore.MaxAge(300) - var requestedURL = r.URL.Query().Get("url") if requestedURL != "" { - http.Redirect(w, r, requestedURL, 302); + redirect302(w, r, requestedURL) } else { renderIndex(w, "you have been logged out") } @@ -308,8 +307,8 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) { // requestedURL comes from nginx in the query string via a 302 redirect // it sets the ultimate destination // https://lasso.yoursite.com/login?url= - var requestedURL = r.URL.Query().Get("url"); - if (requestedURL == "") { + var requestedURL = r.URL.Query().Get("url") + if requestedURL == "" { renderIndex(w, "no destination URL requested") log.Error("no destination URL requested") return @@ -338,7 +337,7 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) { var lURL = loginURL(r, state) log.Debugf("redirecting to oauthURL %s", lURL) context.WithValue(r.Context(), lctx.StatusCode, 302) - http.Redirect(w, r, lURL, 302) + redirect302(w, r, lURL) } } @@ -427,7 +426,7 @@ func CallbackHandler(w http.ResponseWriter, r *http.Request) { // and redirect context.WithValue(r.Context(), lctx.StatusCode, 302) - http.Redirect(w, r, requestedURL, 302) + redirect302(w, r, requestedURL) return } // otherwise serve an html page @@ -585,3 +584,12 @@ func getUserInfoFromIndieAuth(r *http.Request, user *structs.User) error { log.Debug(user) return nil } + +func redirect302(w http.ResponseWriter, r *http.Request, rURL string) { + if cfg.Cfg.Testing { + cfg.Cfg.TestURL = rURL + renderIndex(w, "302 redirect to: "+cfg.Cfg.TestURL) + return + } + http.Redirect(w, r, rURL, 302) +} diff --git a/pkg/cfg/cfg.go b/pkg/cfg/cfg.go index 44c23480..1fe61b1f 100644 --- a/pkg/cfg/cfg.go +++ b/pkg/cfg/cfg.go @@ -42,6 +42,7 @@ type CfgT struct { Name string `mapstructure:"name"` } TestURL string `mapstructure:"test_url"` + Testing bool `mapstructure:"testing"` } // Cfg the main exported config variable From 9980c842d5538074251c08eb77ed95ac2c8f99b9 Mon Sep 17 00:00:00 2001 From: Benjamin Foote Date: Wed, 3 Oct 2018 12:17:35 -0700 Subject: [PATCH 034/360] fix merge conflict --- handlers/handlers.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/handlers/handlers.go b/handlers/handlers.go index 26a89159..7b79e2f1 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -279,7 +279,7 @@ func LogoutHandler(w http.ResponseWriter, r *http.Request) { var requestedURL = r.URL.Query().Get("url") if requestedURL != "" { - http.Redirect(w, r, requestedURL, 302) + redirect302(w, r, requestedURL) } else { renderIndex(w, "you have been logged out") } @@ -337,7 +337,7 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) { var lURL = loginURL(r, state) log.Debugf("redirecting to oauthURL %s", lURL) context.WithValue(r.Context(), lctx.StatusCode, 302) - http.Redirect(w, r, lURL, 302) + redirect302(w, r, lURL) } } @@ -429,7 +429,7 @@ func CallbackHandler(w http.ResponseWriter, r *http.Request) { // and redirect context.WithValue(r.Context(), lctx.StatusCode, 302) - http.Redirect(w, r, requestedURL, 302) + redirect302(w, r, requestedURL) return } // otherwise serve an html page @@ -595,3 +595,12 @@ func getUserInfoFromIndieAuth(r *http.Request, user *structs.User) error { log.Debug(user) return nil } + +func redirect302(w http.ResponseWriter, r *http.Request, rURL string) { + if cfg.Cfg.Testing { + cfg.Cfg.TestURL = rURL + renderIndex(w, "302 redirect to: "+cfg.Cfg.TestURL) + return + } + http.Redirect(w, r, rURL, 302) +} From 1741b747b822b537c0fc21baf48069880376753d Mon Sep 17 00:00:00 2001 From: Benjamin Foote Date: Mon, 8 Oct 2018 17:20:01 -0700 Subject: [PATCH 035/360] ticket #16 add Username as identifier and populate accordingly --- handlers/handlers.go | 67 +++++++++++++++++++++--------------- pkg/jwtmanager/jwtmanager.go | 19 +++++----- pkg/structs/structs.go | 22 ++++++++++++ 3 files changed, 72 insertions(+), 36 deletions(-) diff --git a/handlers/handlers.go b/handlers/handlers.go index 7b79e2f1..c3e01c20 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -210,16 +210,16 @@ func ValidateRequestHandler(w http.ResponseWriter, r *http.Request) { } return } - if claims.Email == "" { + if claims.Username == "" { // no email in jwt if !cfg.Cfg.PublicAccess { - error401(w, r, AuthError{"no email found in jwt", jwt}) + error401(w, r, AuthError{"no Username found in jwt", jwt}) } else { w.Header().Add("X-Lasso-User", "") } return } - log.Infof("email from jwt cookie: %s", claims.Email) + log.Infof("email from jwt cookie: %s", claims.Username) if !cfg.Cfg.AllowAllUsers { if !jwtmanager.SiteInClaims(r.Host, &claims) { @@ -233,10 +233,10 @@ func ValidateRequestHandler(w http.ResponseWriter, r *http.Request) { } // renderIndex(w, "user found from email "+user.Email) - w.Header().Add("X-Lasso-User", claims.Email) + w.Header().Add("X-Lasso-User", claims.Username) w.Header().Add("X-Lasso-Success", "true") log.Debugf("X-Lasso-User response headers %s", w.Header().Get("X-Lasso-User")) - renderIndex(w, "user found in jwt "+claims.Email) + renderIndex(w, "/validate user found in jwt "+claims.Username) // TODO // parse the jwt and see if the claim is valid for the domain @@ -281,7 +281,7 @@ func LogoutHandler(w http.ResponseWriter, r *http.Request) { if requestedURL != "" { redirect302(w, r, requestedURL) } else { - renderIndex(w, "you have been logged out") + renderIndex(w, "/logout you have been logged out") } } @@ -309,14 +309,15 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) { // https://lasso.yoursite.com/login?url= var requestedURL = r.URL.Query().Get("url") if requestedURL == "" { - renderIndex(w, "no destination URL requested") + renderIndex(w, "/login no destination URL requested") log.Error("no destination URL requested") return - } else { - session.Values["requestedURL"] = requestedURL - log.Debugf("session requestedURL set to %s", session.Values["requestedURL"]) } + // set session variable for eventual 302 redirecton to orginal request + session.Values["requestedURL"] = requestedURL + log.Debugf("session requestedURL set to %s", session.Values["requestedURL"]) + // stop them after three failures for this URL var failcount = 0 if session.Values[requestedURL] != nil { @@ -331,7 +332,7 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) { if failcount > 2 { var lassoError = r.URL.Query().Get("error") - renderIndex(w, "too many redirects for "+requestedURL+" - "+lassoError) + renderIndex(w, "/login too many redirects for "+requestedURL+" - "+lassoError) } else { // bounce to oauth provider for login var lURL = loginURL(r, state) @@ -360,14 +361,15 @@ func VerifyUser(u interface{}) (ok bool, err error) { if cfg.Cfg.AllowAllUsers { ok = true + log.Debugf("skipping verify user since cfg.Cfg.AllowAllUsers is %t", cfg.Cfg.AllowAllUsers) // if we're not allowing all users, and we have domains configured and this email isn't in one of those domains... } else if len(cfg.Cfg.Domains) != 0 && !domains.IsUnderManagement(user.Email) { err = fmt.Errorf("Email %s is not within a lasso managed domain", user.Email) // } else if !domains.IsUnderManagement(user.HostDomain) { // err = fmt.Errorf("HostDomain %s is not within a lasso managed domain", u.HostDomain) } else { - log.Debugf("no domains configured") ok = true + log.Debug("no domains configured") } return ok, err } @@ -391,22 +393,22 @@ func CallbackHandler(w http.ResponseWriter, r *http.Request) { queryState := r.URL.Query().Get("state") if session.Values["state"] != queryState { log.Errorf("Invalid session state: stored %s, returned %s", session.Values["state"], queryState) - renderIndex(w, "Invalid session state.") + renderIndex(w, "/auth Invalid session state.") return } user := structs.User{} - if err := getUserInfo(r, &user); err != nil { log.Error(err) http.Error(w, err.Error(), http.StatusBadRequest) return } + log.Debug("CallbackHandler") log.Debug(user) if ok, err := VerifyUser(user); !ok { log.Error(err) - renderIndex(w, fmt.Sprintf("User is not authorized. %s Please try again.", err)) + renderIndex(w, fmt.Sprintf("/auth User is not authorized. %s Please try again.", err)) return } @@ -433,7 +435,7 @@ func CallbackHandler(w http.ResponseWriter, r *http.Request) { return } // otherwise serve an html page - renderIndex(w, tokenstring) + renderIndex(w, "/auth "+tokenstring) } // TODO: put all getUserInfo logic into its own pkg @@ -478,6 +480,7 @@ func getUserInfoFromOpenID(client *http.Client, user *structs.User, ptoken *oaut // c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{"message": }) return err } + user.PrepareUserData() return nil } @@ -496,6 +499,8 @@ func getUserInfoFromGoogle(client *http.Client, user *structs.User) error { // c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{"message": }) return err } + user.PrepareUserData() + return nil } @@ -503,11 +508,13 @@ func getUserInfoFromGoogle(client *http.Client, user *structs.User) error { // https://developer.github.com/apps/building-integrations/setting-up-and-registering-oauth-apps/about-authorization-options-for-oauth-apps/ func getUserInfoFromGithub(client *http.Client, user *structs.User, ptoken *oauth2.Token) error { - log.Errorf("ptoken.AccessToken: %s", ptoken.AccessToken) + // TODO: move to cfg package userInfoURL := "https://api.github.com/user?access_token=" if genOauth.UserInfoURL != "" { userInfoURL = genOauth.UserInfoURL } + + log.Errorf("ptoken.AccessToken: %s", ptoken.AccessToken) userinfo, err := client.Get(userInfoURL + ptoken.AccessToken) if err != nil { // http.Error(w, err.Error(), http.StatusBadRequest) @@ -516,19 +523,23 @@ func getUserInfoFromGithub(client *http.Client, user *structs.User, ptoken *oaut defer userinfo.Body.Close() data, _ := ioutil.ReadAll(userinfo.Body) log.Println("github userinfo body: ", string(data)) - ghUser := &structs.GithubUser{} - if err = json.Unmarshal(data, ghUser); err != nil { - // if err = json.Unmarshal(data, user); err != nil { + ghUser := structs.GithubUser{} + if err = json.Unmarshal(data, &ghUser); err != nil { log.Errorln(err) - // renderIndex(w, "Error marshalling response. Please try agian.") - // c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{"message": }) return err } - ghUser.Email = ghUser.Login - if ghUser.Email == "" && ghUser.Login != "" { - log.Debug("no email returned from github, setting user email to login " + ghUser.Login) - } - user = &ghUser.User + log.Debug("getUserInfoFromGithub ghUser") + log.Debug(ghUser) + log.Debug("getUserInfoFromGithub user") + log.Debug(user) + + ghUser.PrepareUserData() + user.Email = ghUser.Email + user.Name = ghUser.Name + user.Username = ghUser.Username + user.ID = ghUser.ID + // user = &ghUser.User + log.Debug("getUserInfoFromGithub") log.Debug(user) return nil @@ -598,8 +609,10 @@ func getUserInfoFromIndieAuth(r *http.Request, user *structs.User) error { func redirect302(w http.ResponseWriter, r *http.Request, rURL string) { if cfg.Cfg.Testing { + var tmp = cfg.Cfg.TestURL cfg.Cfg.TestURL = rURL renderIndex(w, "302 redirect to: "+cfg.Cfg.TestURL) + cfg.Cfg.TestURL = tmp return } http.Redirect(w, r, rURL, 302) diff --git a/pkg/jwtmanager/jwtmanager.go b/pkg/jwtmanager/jwtmanager.go index f15ad60e..04126902 100644 --- a/pkg/jwtmanager/jwtmanager.go +++ b/pkg/jwtmanager/jwtmanager.go @@ -10,9 +10,9 @@ import ( "strings" "time" - log "github.com/Sirupsen/logrus" "github.com/LassoProject/lasso/pkg/cfg" "github.com/LassoProject/lasso/pkg/structs" + log "github.com/Sirupsen/logrus" jwt "github.com/dgrijalva/jwt-go" ) @@ -21,8 +21,8 @@ import ( // LassoClaims jwt Claims specific to lasso type LassoClaims struct { - Email string `json:"email"` - Sites []string `json:"sites"` // tempting to make this a map but the array is fewer characters in the jwt + Username string `json:"username"` + Sites []string `json:"sites"` // tempting to make this a map but the array is fewer characters in the jwt jwt.StandardClaims } @@ -46,8 +46,9 @@ func init() { // CreateUserTokenString converts user to signed jwt func CreateUserTokenString(u structs.User) string { // User`token` + // u.PrepareUserData() claims := LassoClaims{ - u.Email, + u.Username, Sites, StandardClaims, } @@ -135,8 +136,8 @@ func SiteInClaims(site string, claims *LassoClaims) bool { return false } -// TODO HERE there's something wrong with claims parsing, probably related to LassoClaims not being a pointer // PTokenClaims get all the claims +// TODO HERE there's something wrong with claims parsing, probably related to LassoClaims not being a pointer func PTokenClaims(ptoken *jwt.Token) (LassoClaims, error) { // func PTokenClaims(ptoken *jwt.Token) (LassoClaims, error) { // return ptoken.Claims, nil @@ -151,9 +152,9 @@ func PTokenClaims(ptoken *jwt.Token) (LassoClaims, error) { return *ptokenClaims, nil } -// PTokenToEmail returns the Email in the validated ptoken -func PTokenToEmail(ptoken *jwt.Token) (string, error) { - return ptoken.Claims.(*LassoClaims).Email, nil +// PTokenToUsername returns the Username in the validated ptoken +func PTokenToUsername(ptoken *jwt.Token) (string, error) { + return ptoken.Claims.(*LassoClaims).Username, nil // var ptokenClaims LassoClaims // ptokenClaims, err := PTokenClaims(ptoken) @@ -161,7 +162,7 @@ func PTokenToEmail(ptoken *jwt.Token) (string, error) { // log.Error(err) // return "", err // } - // return ptokenClaims.Email, nil + // return ptokenClaims.Username, nil } func decodeAndDecompressTokenString(encgzipss string) string { diff --git a/pkg/structs/structs.go b/pkg/structs/structs.go index 1f6856ae..43eca09d 100644 --- a/pkg/structs/structs.go +++ b/pkg/structs/structs.go @@ -1,15 +1,26 @@ package structs +// UserI each *User struct must prepare the data for being placed in the JWT +type UserI interface { + PrepareUserData() +} + // User is inherited. type User struct { Name string `json:"name"` Email string `json:"email"` CreatedOn int64 `json:"createdon"` LastUpdate int64 `json:"lastupdate"` + Username string `json:"username",mapstructure:"username"` ID int `json:"id",mapstructure:"id"` // jwt.StandardClaims } +// PrepareUserData implement PersonalData interface +func (u *User) PrepareUserData() { + u.Username = u.Email +} + // GoogleUser is a retrieved and authentiacted user from Google. // unused! type GoogleUser struct { @@ -25,6 +36,11 @@ type GoogleUser struct { // jwt.StandardClaims } +// PrepareUserData implement PersonalData interface +func (u *GoogleUser) PrepareUserData() { + u.Username = u.Email +} + // GithubUser is a retrieved and authentiacted user from Github. type GithubUser struct { User @@ -33,6 +49,12 @@ type GithubUser struct { // jwt.StandardClaims } +// PrepareUserData implement PersonalData interface +func (u *GithubUser) PrepareUserData() { + // always use the u.Login as the u.Username + u.Username = u.Login +} + // GenericOauth provides endoint for access type GenericOauth struct { ClientID string `mapstructure:"client_id"` From 567cce096a13e2f1d3c191967edb41e7bd8fcdef Mon Sep 17 00:00:00 2001 From: Benjamin Foote Date: Tue, 9 Oct 2018 12:55:23 -0700 Subject: [PATCH 036/360] #16 don't enforce oauth.client_secret config --- pkg/cfg/cfg.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cfg/cfg.go b/pkg/cfg/cfg.go index 1fe61b1f..bcfa5a99 100644 --- a/pkg/cfg/cfg.go +++ b/pkg/cfg/cfg.go @@ -49,7 +49,7 @@ type CfgT struct { var Cfg CfgT // RequiredOptions must have these fields set for minimum viable config -var RequiredOptions = []string{"lasso.port", "lasso.listen", "lasso.jwt.secret", "lasso.db.file", "oauth.provider", "oauth.client_id", "oauth.client_secret"} +var RequiredOptions = []string{"lasso.port", "lasso.listen", "lasso.jwt.secret", "lasso.db.file", "oauth.provider", "oauth.client_id"} func init() { ParseConfig() From 9b243f9f81e52b572cc416de22fe08177b6b1084 Mon Sep 17 00:00:00 2001 From: Benjamin Foote Date: Tue, 9 Oct 2018 13:28:31 -0700 Subject: [PATCH 037/360] #16 call PrepareUserData to set Username --- handlers/handlers.go | 1 + 1 file changed, 1 insertion(+) diff --git a/handlers/handlers.go b/handlers/handlers.go index c3e01c20..239ae67d 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -603,6 +603,7 @@ func getUserInfoFromIndieAuth(r *http.Request, user *structs.User) error { return err } user.Email = ir.Email + user.PrepareUserData() log.Debug(user) return nil } From afc0a41824a29e4f1dd9a144296b3cd2d9768898 Mon Sep 17 00:00:00 2001 From: Aaron Parecki Date: Tue, 9 Oct 2018 17:14:17 -0400 Subject: [PATCH 038/360] refactor indieauth user response to match the github format both indieauth and github providers return the user identifier in the auth code exchange response --- handlers/handlers.go | 14 ++++---------- pkg/structs/structs.go | 9 +++++++++ 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/handlers/handlers.go b/handlers/handlers.go index 239ae67d..a2ced701 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -545,12 +545,6 @@ func getUserInfoFromGithub(client *http.Client, user *structs.User, ptoken *oaut return nil } -// indieauth -// https://indieauth.com/developers -type indieResponse struct { - Email string `json:"me"` -} - func getUserInfoFromIndieAuth(r *http.Request, user *structs.User) error { code := r.URL.Query().Get("code") @@ -597,13 +591,13 @@ func getUserInfoFromIndieAuth(r *http.Request, user *structs.User) error { defer userinfo.Body.Close() data, _ := ioutil.ReadAll(userinfo.Body) log.Println("indieauth userinfo body: ", string(data)) - ir := indieResponse{} - if err := json.Unmarshal(data, &ir); err != nil { + iaUser := structs.IndieAuthUser{} + if err = json.Unmarshal(data, &iaUser); err != nil { log.Errorln(err) return err } - user.Email = ir.Email - user.PrepareUserData() + iaUser.PrepareUserData() + user.Username = iaUser.Username log.Debug(user) return nil } diff --git a/pkg/structs/structs.go b/pkg/structs/structs.go index 43eca09d..9014337c 100644 --- a/pkg/structs/structs.go +++ b/pkg/structs/structs.go @@ -55,6 +55,15 @@ func (u *GithubUser) PrepareUserData() { u.Username = u.Login } +type IndieAuthUser struct { + User + URL string `json:"me"` +} + +func (u *IndieAuthUser) PrepareUserData() { + u.Username = u.URL +} + // GenericOauth provides endoint for access type GenericOauth struct { ClientID string `mapstructure:"client_id"` From 4d294251832d73cb88daf3237c1ecd27a3759ad0 Mon Sep 17 00:00:00 2001 From: Aaron Parecki Date: Tue, 9 Oct 2018 17:14:40 -0400 Subject: [PATCH 039/360] move User.Username to the top of the struct Username is the canonical user identifier across providers, so move it to the top so that it's more visible --- pkg/structs/structs.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/structs/structs.go b/pkg/structs/structs.go index 9014337c..0e59f97d 100644 --- a/pkg/structs/structs.go +++ b/pkg/structs/structs.go @@ -7,11 +7,11 @@ type UserI interface { // User is inherited. type User struct { + Username string `json:"username",mapstructure:"username"` Name string `json:"name"` Email string `json:"email"` CreatedOn int64 `json:"createdon"` LastUpdate int64 `json:"lastupdate"` - Username string `json:"username",mapstructure:"username"` ID int `json:"id",mapstructure:"id"` // jwt.StandardClaims } From 75450fbc8218473eafa7e8550980626a1c12a4b8 Mon Sep 17 00:00:00 2001 From: Aaron Parecki Date: Tue, 9 Oct 2018 17:28:47 -0400 Subject: [PATCH 040/360] override response_type=id for IndieAuth providers this avoids requesting an access token since we are just trying to authenticate, not trying to authorize. see https://indieauth.spec.indieweb.org/#authentication for details --- handlers/handlers.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/handlers/handlers.go b/handlers/handlers.go index a2ced701..1ec48f98 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -107,6 +107,8 @@ func loginURL(r *http.Request, state string) string { } } url = oauthclient.AuthCodeURL(state, oauthopts) + } else if genOauth.Provider == "indieauth" { + url = oauthclient.AuthCodeURL(state, oauth2.SetAuthURLParam("response_type", "id")) } else { url = oauthclient.AuthCodeURL(state) } From ca721b60fd9e03633c3e69968229a2bd393355a3 Mon Sep 17 00:00:00 2001 From: Aaron Parecki Date: Tue, 9 Oct 2018 17:28:55 -0400 Subject: [PATCH 041/360] update readme to clarify indieauth support --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 96d9e4be..0f8aefaa 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Lasso -an SSO solution for nginx using the [auth_request](http://nginx.org/en/docs/http/ngx_http_auth_request_module.html) module +an SSO solution for nginx using the [auth_request](http://nginx.org/en/docs/http/ngx_http_auth_request_module.html) module. -lasso supports OAuth login to google apps, [github](https://developer.github.com/apps/building-integrations/setting-up-and-registering-oauth-apps/about-authorization-options-for-oauth-apps/) and [indieauth](https://indieauth.com/developers) +lasso supports OAuth login via Google, [GitHub](https://developer.github.com/apps/building-integrations/setting-up-and-registering-oauth-apps/about-authorization-options-for-oauth-apps/), [IndieAuth](https://indieauth.spec.indieweb.org/), and OpenID Connect providers. If lasso is running on the same host as the nginx reverse proxy the response time from the `/validate` endpoint to nginx should be less than 1ms From 26fab067715d95974f7f70b4aa1eb4cd2b3d8c1e Mon Sep 17 00:00:00 2001 From: Benjamin Foote Date: Thu, 11 Oct 2018 16:17:34 -0700 Subject: [PATCH 042/360] only start ws interface if configured to do such --- main.go | 6 +++++- pkg/transciever/transciever.go | 6 +++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/main.go b/main.go index 764cb977..5330ab85 100644 --- a/main.go +++ b/main.go @@ -38,7 +38,11 @@ func main() { // router.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("./static")))) mux.Handle("/static", http.FileServer(http.Dir("./static"))) - mux.Handle("/ws", tran.WS) + if cfg.Cfg.WebApp { + log.Info("enabling websocket") + tran.ExplicitInit() + mux.Handle("/ws", tran.WS) + } // socketio := tran.NewServer() // mux.Handle("/socket.io/", cors.AllowAll(socketio)) diff --git a/pkg/transciever/transciever.go b/pkg/transciever/transciever.go index 36537db9..9925636d 100644 --- a/pkg/transciever/transciever.go +++ b/pkg/transciever/transciever.go @@ -20,9 +20,9 @@ var hh = &HubHolder{ Hub: newHub(), } -// NewHub -func init() { - log.Info("hub %v", hh.Hub) +// ExplicitInit only run init() if we're configured for such +func ExplicitInit() { + log.Debug("hub %v", hh.Hub) go hh.Hub.run() } From cd48cc479d2c11586be2db75fa3167c41cb3b223 Mon Sep 17 00:00:00 2001 From: Benjamin Foote Date: Thu, 11 Oct 2018 16:18:49 -0700 Subject: [PATCH 043/360] cleanup degug --- pkg/timelog/timelog.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/timelog/timelog.go b/pkg/timelog/timelog.go index 8c149b65..810e82ee 100644 --- a/pkg/timelog/timelog.go +++ b/pkg/timelog/timelog.go @@ -43,7 +43,8 @@ func TimeLog(nextHandler http.Handler) http.HandlerFunc { // var statusCode int // var statusColor string statusCode := ctx.Value(lctx.StatusCode) - log.Debugf("statuscode: %v", statusCode) + // TODO: this just doesn't seem to work, how can we get the statusCode from the context? + // log.Debugf("statuscode: %v", statusCode) if statusCode == nil { statusCode = 200 } From 2c35b2c113a69b0ccdc98d138164d4c61d229a2e Mon Sep 17 00:00:00 2001 From: Benjamin Foote Date: Thu, 11 Oct 2018 16:19:21 -0700 Subject: [PATCH 044/360] sort domains by length desc --- pkg/domains/domains.go | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/pkg/domains/domains.go b/pkg/domains/domains.go index aec4bd59..bee5a86d 100644 --- a/pkg/domains/domains.go +++ b/pkg/domains/domains.go @@ -1,19 +1,24 @@ package domains import ( + "sort" "strings" "github.com/LassoProject/lasso/pkg/cfg" log "github.com/Sirupsen/logrus" ) -// TODO sort domains by length from longest to shortest -// https://play.golang.org/p/N6GbEgBffd +var domains = cfg.Cfg.Domains + +func init() { + sort.Sort(ByLengthDesc(domains)) +} // Matches returns one of the domains we're configured for // TODO return all matches +// Matches return the first match of the func Matches(s string) string { - for i, v := range cfg.Cfg.Domains { + for i, v := range domains { log.Debugf("domain matched array value at [%d]=%v", i, v) if strings.Contains(s, v) { return v @@ -30,3 +35,19 @@ func IsUnderManagement(s string) bool { } return false } + +// ByLengthDesc sort from +// https://play.golang.org/p/N6GbEgBffd +type ByLengthDesc []string + +func (s ByLengthDesc) Len() int { + return len(s) +} +func (s ByLengthDesc) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +// this differs by offing the longest first +func (s ByLengthDesc) Less(i, j int) bool { + return len(s[j]) < len(s[i]) +} From c3f93955c3b9b49e0a6f2a0af4239c406e21b461 Mon Sep 17 00:00:00 2001 From: Benjamin Foote Date: Thu, 11 Oct 2018 16:21:17 -0700 Subject: [PATCH 045/360] #23 email --> username --- pkg/model/user.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/model/user.go b/pkg/model/user.go index 06897c22..34bf44fe 100644 --- a/pkg/model/user.go +++ b/pkg/model/user.go @@ -6,8 +6,8 @@ import ( "fmt" "time" - log "github.com/Sirupsen/logrus" "github.com/LassoProject/lasso/pkg/structs" + log "github.com/Sirupsen/logrus" "github.com/boltdb/bolt" ) @@ -15,7 +15,7 @@ import ( func PutUser(u structs.User) error { userexists := false curu := &structs.User{} - err := User([]byte(u.Email), curu) + err := User([]byte(u.Username), curu) if err == nil { userexists = true } else { @@ -42,7 +42,7 @@ func PutUser(u structs.User) error { return err } - err = b.Put([]byte(u.Email), eU) + err = b.Put([]byte(u.Username), eU) if err != nil { log.Error(err) return err @@ -63,7 +63,7 @@ func User(key []byte, u *structs.User) error { return err } *u = *user - log.Debugf("retrieved %s from db", u.Email) + log.Debugf("retrieved %s from db", u.Username) return nil } return fmt.Errorf("no bucket for %s", userBucket) From 88e1dca690936db6a8e605cf66021687d8d81592 Mon Sep 17 00:00:00 2001 From: Benjamin Foote Date: Thu, 11 Oct 2018 16:27:56 -0700 Subject: [PATCH 046/360] fix #25 set defaults for config items --- handlers/handlers.go | 138 +++++++------------ pkg/cfg/cfg.go | 258 +++++++++++++++++++++++++++++++++-- pkg/jwtmanager/jwtmanager.go | 5 +- pkg/model/model.go | 13 +- pkg/model/model_test.go | 8 +- pkg/structs/structs.go | 32 ++--- 6 files changed, 322 insertions(+), 132 deletions(-) diff --git a/handlers/handlers.go b/handlers/handlers.go index 1ec48f98..8b2aa8ad 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -24,10 +24,10 @@ import ( "github.com/LassoProject/lasso/pkg/structs" "github.com/gorilla/sessions" "golang.org/x/oauth2" - "golang.org/x/oauth2/google" ) // Index variables passed to index.tmpl +// TODO: turn TestURL into an array of URLs to display type Index struct { Msg string TestURL string @@ -40,50 +40,16 @@ type AuthError struct { } var ( - genOauth structs.GenericOauth - oauthclient *oauth2.Config - oauthopts oauth2.AuthCodeOption + //oauthclient = cfg.OAuthClient*oauth2.Config TODO: remove // Templates with functions available to them indexTemplate = template.Must(template.ParseFiles("./templates/index.tmpl")) - - sessstore = sessions.NewCookieStore([]byte(cfg.Cfg.Session.Name)) + sessstore = sessions.NewCookieStore([]byte(cfg.Cfg.Session.Name)) ) -func init() { - log.Debug("init handlers") - - err := cfg.UnmarshalKey("oauth", &genOauth) - if err == nil { - if genOauth.Provider == "google" { - log.Info("configuring google oauth") - oauthclient = &oauth2.Config{ - ClientID: genOauth.ClientID, - ClientSecret: genOauth.ClientSecret, - Scopes: []string{ - // You have to select a scope from - // https://developers.google.com/identity/protocols/googlescopes#google_sign-in - "https://www.googleapis.com/auth/userinfo.email", - }, - Endpoint: google.Endpoint, - } - log.Infof("setting google oauth preferred login domain param 'hd' to %s", genOauth.PreferredDomain) - oauthopts = oauth2.SetAuthURLParam("hd", genOauth.PreferredDomain) - } else { - log.Info("configuring generic oauth") - oauthclient = &oauth2.Config{ - ClientID: genOauth.ClientID, - ClientSecret: genOauth.ClientSecret, - Endpoint: oauth2.Endpoint{ - AuthURL: genOauth.AuthURL, - TokenURL: genOauth.TokenURL, - }, - RedirectURL: genOauth.RedirectURL, - Scopes: genOauth.Scopes, - } - } - } -} +// func init() { +// log.Debug("handlers ") +// } func randString() string { b := make([]byte, 32) @@ -95,29 +61,29 @@ func loginURL(r *http.Request, state string) string { // State can be some kind of random generated hash string. // See relevant RFC: http://tools.ietf.org/html/rfc6749#section-10.12 var url = "" - if genOauth.Provider == "google" { + if cfg.GenOAuth.Provider == cfg.Providers.Google { // If the provider is Google, find a matching redirect URL to use for the client domain := domains.Matches(r.Host) log.Debugf("looking for redirect URL matching %v", domain) - for i, v := range genOauth.RedirectURLs { - log.Debugf("redirect value matched at [%d]=%v", i, v) + for i, v := range cfg.GenOAuth.RedirectURLs { if strings.Contains(v, domain) { - oauthclient.RedirectURL = v + log.Debugf("redirect value matched at [%d]=%v", i, v) + cfg.OAuthClient.RedirectURL = v break } } - url = oauthclient.AuthCodeURL(state, oauthopts) - } else if genOauth.Provider == "indieauth" { - url = oauthclient.AuthCodeURL(state, oauth2.SetAuthURLParam("response_type", "id")) + url = cfg.OAuthClient.AuthCodeURL(state, cfg.OAuthopts) + } else if cfg.GenOAuth.Provider == cfg.Providers.IndieAuth { + url = cfg.OAuthClient.AuthCodeURL(state, oauth2.SetAuthURLParam("response_type", "id")) } else { - url = oauthclient.AuthCodeURL(state) + url = cfg.OAuthClient.AuthCodeURL(state) } // log.Debugf("loginUrl %s", url) return url } -// FindJWT look for JWT in Cookie, JWT Header, Authorization Header (OAuth 2 Bearer Token) +// FindJWT look for JWT in Cookie, JWT Header, Authorization Header (OAuth2 Bearer Token) // and Query String in that order func FindJWT(r *http.Request) string { jwt, err := cookie.Cookie(r) @@ -195,9 +161,9 @@ func ValidateRequestHandler(w http.ResponseWriter, r *http.Request) { if jwt == "" { // If the module is configured to allow public access with no authentication, return 200 now if !cfg.Cfg.PublicAccess { - error401(w, r, AuthError{Error: "no jwt found"}) + error401(w, r, AuthError{Error: "no jwt found in request for "}) } else { - w.Header().Add("X-Lasso-User", "") + w.Header().Add(cfg.Cfg.Headers.User, "") } return } @@ -208,7 +174,7 @@ func ValidateRequestHandler(w http.ResponseWriter, r *http.Request) { if !cfg.Cfg.PublicAccess { error401(w, r, AuthError{err.Error(), jwt}) } else { - w.Header().Add("X-Lasso-User", "") + w.Header().Add(cfg.Cfg.Headers.User, "") } return } @@ -217,27 +183,27 @@ func ValidateRequestHandler(w http.ResponseWriter, r *http.Request) { if !cfg.Cfg.PublicAccess { error401(w, r, AuthError{"no Username found in jwt", jwt}) } else { - w.Header().Add("X-Lasso-User", "") + w.Header().Add(cfg.Cfg.Headers.User, "") } return } - log.Infof("email from jwt cookie: %s", claims.Username) + log.Infof("username from jwt cookie: %s", claims.Username) if !cfg.Cfg.AllowAllUsers { if !jwtmanager.SiteInClaims(r.Host, &claims) { if !cfg.Cfg.PublicAccess { error401(w, r, AuthError{"not authorized for " + r.Host, jwt}) } else { - w.Header().Add("X-Lasso-User", "") + w.Header().Add(cfg.Cfg.Headers.User, "") } return } } // renderIndex(w, "user found from email "+user.Email) - w.Header().Add("X-Lasso-User", claims.Username) - w.Header().Add("X-Lasso-Success", "true") - log.Debugf("X-Lasso-User response headers %s", w.Header().Get("X-Lasso-User")) + w.Header().Add(cfg.Cfg.Headers.User, claims.Username) + w.Header().Add(cfg.Cfg.Headers.Success, "true") + log.Debugf("response header "+cfg.Cfg.Headers.User+": %s", w.Header().Get(cfg.Cfg.Headers.User)) renderIndex(w, "/validate user found in jwt "+claims.Username) // TODO @@ -377,7 +343,7 @@ func VerifyUser(u interface{}) (ok bool, err error) { } // CallbackHandler /auth -// - validate info from oauth provider (Google, Github, OIDC, etc) +// - validate info from oauth provider (Google, GitHub, OIDC, etc) // - create user // - issue jwt in the form of a cookie func CallbackHandler(w http.ResponseWriter, r *http.Request) { @@ -445,22 +411,22 @@ func CallbackHandler(w http.ResponseWriter, r *http.Request) { func getUserInfo(r *http.Request, user *structs.User) error { // indieauth sends the "me" setting in json back to the callback, so just pluck it from the callback - if genOauth.Provider == "indieauth" { + if cfg.GenOAuth.Provider == "indieauth" { return getUserInfoFromIndieAuth(r, user) } - providerToken, err := oauthclient.Exchange(oauth2.NoContext, r.URL.Query().Get("code")) + providerToken, err := cfg.OAuthClient.Exchange(oauth2.NoContext, r.URL.Query().Get("code")) if err != nil { return err } // make the "third leg" request back to google to exchange the token for the userinfo - client := oauthclient.Client(oauth2.NoContext, providerToken) - if genOauth.Provider == "google" { + client := cfg.OAuthClient.Client(oauth2.NoContext, providerToken) + if cfg.GenOAuth.Provider == cfg.Providers.Google { return getUserInfoFromGoogle(client, user) - } else if genOauth.Provider == "github" { - return getUserInfoFromGithub(client, user, providerToken) - } else if genOauth.Provider == "oidc" { + } else if cfg.GenOAuth.Provider == cfg.Providers.GitHub { + return getUserInfoFromGitHub(client, user, providerToken) + } else if cfg.GenOAuth.Provider == cfg.Providers.OIDC { return getUserInfoFromOpenID(client, user, providerToken) } log.Error("we don't know how to look up the user info") @@ -468,9 +434,8 @@ func getUserInfo(r *http.Request, user *structs.User) error { } func getUserInfoFromOpenID(client *http.Client, user *structs.User, ptoken *oauth2.Token) error { - userinfo, err := client.Get(genOauth.UserInfoURL) + userinfo, err := client.Get(cfg.GenOAuth.UserInfoURL) if err != nil { - // http.Error(w, err.Error(), http.StatusBadRequest) return err } defer userinfo.Body.Close() @@ -478,8 +443,6 @@ func getUserInfoFromOpenID(client *http.Client, user *structs.User, ptoken *oaut log.Println("OpenID userinfo body: ", string(data)) if err = json.Unmarshal(data, user); err != nil { log.Errorln(err) - // renderIndex(w, "Error marshalling response. Please try agian.") - // c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{"message": }) return err } user.PrepareUserData() @@ -487,9 +450,8 @@ func getUserInfoFromOpenID(client *http.Client, user *structs.User, ptoken *oaut } func getUserInfoFromGoogle(client *http.Client, user *structs.User) error { - userinfo, err := client.Get("https://www.googleapis.com/oauth2/v3/userinfo") + userinfo, err := client.Get(cfg.GenOAuth.UserInfoURL) if err != nil { - // http.Error(w, err.Error(), http.StatusBadRequest) return err } defer userinfo.Body.Close() @@ -497,8 +459,6 @@ func getUserInfoFromGoogle(client *http.Client, user *structs.User) error { log.Println("google userinfo body: ", string(data)) if err = json.Unmarshal(data, user); err != nil { log.Errorln(err) - // renderIndex(w, "Error marshalling response. Please try agian.") - // c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{"message": }) return err } user.PrepareUserData() @@ -508,16 +468,10 @@ func getUserInfoFromGoogle(client *http.Client, user *structs.User) error { // github // https://developer.github.com/apps/building-integrations/setting-up-and-registering-oauth-apps/about-authorization-options-for-oauth-apps/ -func getUserInfoFromGithub(client *http.Client, user *structs.User, ptoken *oauth2.Token) error { - - // TODO: move to cfg package - userInfoURL := "https://api.github.com/user?access_token=" - if genOauth.UserInfoURL != "" { - userInfoURL = genOauth.UserInfoURL - } +func getUserInfoFromGitHub(client *http.Client, user *structs.User, ptoken *oauth2.Token) error { log.Errorf("ptoken.AccessToken: %s", ptoken.AccessToken) - userinfo, err := client.Get(userInfoURL + ptoken.AccessToken) + userinfo, err := client.Get(cfg.GenOAuth.UserInfoURL + ptoken.AccessToken) if err != nil { // http.Error(w, err.Error(), http.StatusBadRequest) return err @@ -525,14 +479,14 @@ func getUserInfoFromGithub(client *http.Client, user *structs.User, ptoken *oaut defer userinfo.Body.Close() data, _ := ioutil.ReadAll(userinfo.Body) log.Println("github userinfo body: ", string(data)) - ghUser := structs.GithubUser{} + ghUser := structs.GitHubUser{} if err = json.Unmarshal(data, &ghUser); err != nil { log.Errorln(err) return err } - log.Debug("getUserInfoFromGithub ghUser") + log.Debug("getUserInfoFromGitHub ghUser") log.Debug(ghUser) - log.Debug("getUserInfoFromGithub user") + log.Debug("getUserInfoFromGitHub user") log.Debug(user) ghUser.PrepareUserData() @@ -542,7 +496,7 @@ func getUserInfoFromGithub(client *http.Client, user *structs.User, ptoken *oaut user.ID = ghUser.ID // user = &ghUser.User - log.Debug("getUserInfoFromGithub") + log.Debug("getUserInfoFromGitHub") log.Debug(user) return nil } @@ -561,19 +515,19 @@ func getUserInfoFromIndieAuth(r *http.Request, user *structs.User) error { if _, err = fw.Write([]byte(code)); err != nil { return err } - // v.Set("redirect_uri", genOauth.RedirectURL) + // v.Set("redirect_uri", cfg.GenOAuth.RedirectURL) fw, err = w.CreateFormField("redirect_uri") - if _, err = fw.Write([]byte(genOauth.RedirectURL)); err != nil { + if _, err = fw.Write([]byte(cfg.GenOAuth.RedirectURL)); err != nil { return err } - // v.Set("client_id", genOauth.ClientID) + // v.Set("client_id", cfg.GenOAuth.ClientID) fw, err = w.CreateFormField("client_id") - if _, err = fw.Write([]byte(genOauth.ClientID)); err != nil { + if _, err = fw.Write([]byte(cfg.GenOAuth.ClientID)); err != nil { return err } w.Close() - req, err := http.NewRequest("POST", genOauth.AuthURL, &b) + req, err := http.NewRequest("POST", cfg.GenOAuth.AuthURL, &b) if err != nil { return err } @@ -581,7 +535,7 @@ func getUserInfoFromIndieAuth(r *http.Request, user *structs.User) error { req.Header.Set("Accept", "application/json") // v := url.Values{} - // userinfo, err := client.PostForm(genOauth.UserInfoURL, v) + // userinfo, err := client.PostForm(cfg.GenOAuth.UserInfoURL, v) client := &http.Client{} userinfo, err := client.Do(req) diff --git a/pkg/cfg/cfg.go b/pkg/cfg/cfg.go index bcfa5a99..8b9f4a5c 100644 --- a/pkg/cfg/cfg.go +++ b/pkg/cfg/cfg.go @@ -3,9 +3,15 @@ package cfg import ( "errors" "flag" + "io/ioutil" + "math/rand" "os" + "time" log "github.com/Sirupsen/logrus" + "golang.org/x/oauth2" + "golang.org/x/oauth2/github" + "golang.org/x/oauth2/google" "github.com/spf13/viper" ) @@ -32,8 +38,10 @@ type CfgT struct { } Headers struct { JWT string `mapstructure:"jwt"` + User string `mapstructure:"user"` QueryString string `mapstructure:"querystring"` Redirect string `mapstructure:"redirect"` + Success string `mapstructure:"success"` } DB struct { File string `mapstructure:"file"` @@ -43,28 +51,77 @@ type CfgT struct { } TestURL string `mapstructure:"test_url"` Testing bool `mapstructure:"testing"` + WebApp bool `mapstructure:"webapp"` } -// Cfg the main exported config variable -var Cfg CfgT +// oauth config items endoint for access +type oauthConfig struct { + Provider string `mapstructure:"provider"` + ClientID string `mapstructure:"client_id"` + ClientSecret string `mapstructure:"client_secret"` + AuthURL string `mapstructure:"auth_url"` + TokenURL string `mapstructure:"token_url"` + RedirectURL string `mapstructure:"callback_url"` + RedirectURLs []string `mapstructure:"callback_urls"` + Scopes []string `mapstructure:"scopes"` + UserInfoURL string `mapstructure:"user_info_url"` + PreferredDomain string `mapstructre:"preferredDomain"` +} + +// OAuthProviders holds the stings for +type OAuthProviders struct { + Google string + GitHub string + IndieAuth string + OIDC string +} + +var ( + // Cfg the main exported config variable + Cfg CfgT + + // GenOAuth exported OAuth config variable + // TODO: I think GenOAuth and OAuthConfig can be combined! + // perhaps by https://golang.org/doc/effective_go.html#embedding + GenOAuth *oauthConfig + + // OAuthClient is the configured client which will call the provider + // this actually carries the oauth2 client ala oauthclient.Client(oauth2.NoContext, providerToken) + OAuthClient *oauth2.Config + // OAuthopts authentication options + OAuthopts oauth2.AuthCodeOption + + // Providers static strings to test against + Providers = &OAuthProviders{ + Google: "google", + GitHub: "github", + IndieAuth: "indieauth", + OIDC: "oidc", + } +) // RequiredOptions must have these fields set for minimum viable config -var RequiredOptions = []string{"lasso.port", "lasso.listen", "lasso.jwt.secret", "lasso.db.file", "oauth.provider", "oauth.client_id"} +var RequiredOptions = []string{"oauth.provider", "oauth.client_id"} func init() { + // from config file ParseConfig() + + // can pass loglevel on the command line var ll = flag.String("loglevel", Cfg.LogLevel, "enable debug log output") flag.Parse() if *ll == "debug" { log.SetLevel(log.DebugLevel) log.Debug("logLevel set to debug") } + + setDefaults() log.Debug(viper.AllSettings()) } // ParseConfig parse the config file func ParseConfig() { - log.Info("opening config") + log.Debug("opening config") viper.SetConfigName("config") viper.SetConfigType("yaml") viper.AddConfigPath(os.Getenv("LASSO_ROOT") + "config") @@ -79,13 +136,6 @@ func ParseConfig() { // log.Fatalf(err.prob) panic(errT) } - // nested defaults is currently *broken* - // https://github.com/spf13/viper/issues/309 - // viper.SetDefault("listen", "0.0.0.0") - // viper.SetDefault(Cfg.Port, 9090) - // viper.SetDefault("Headers.SSO", "X-Lasso-Token") - // viper.SetDefault("Headers.Redirect", "X-Lasso-Requested-URI") - // viper.SetDefault("Cookie.Name", "Lasso") log.Debugf("secret: %s", string(Cfg.JWT.Secret)) } @@ -108,3 +158,189 @@ func BasicTest() error { } return nil } + +// setDefaults set default options for some items +func setDefaults() { + + // this should really be done by Viper up in parseConfig but.. + // nested defaults is currently *broken* + // https://github.com/spf13/viper/issues/309 + // viper.SetDefault("listen", "0.0.0.0") + // viper.SetDefault(Cfg.Port, 9090) + // viper.SetDefault("Headers.SSO", "X-Lasso-Token") + // viper.SetDefault("Headers.Redirect", "X-Lasso-Requested-URI") + // viper.SetDefault("Cookie.Name", "Lasso") + + // logging + if !viper.IsSet("lasso.logLevel") { + Cfg.LogLevel = "info" + } + // network defaults + if !viper.IsSet("lasso.listen") { + Cfg.Listen = "0.0.0.0" + } + if !viper.IsSet("lasso.port") { + Cfg.Port = 9090 + } + if !viper.IsSet("lasso.allowAllUsers") { + Cfg.AllowAllUsers = false + } + if !viper.IsSet("lasso.publicAccess") { + Cfg.PublicAccess = false + } + + // jwt defaults + if !viper.IsSet("lasso.jwt.secret") { + Cfg.JWT.Secret = getOrGenerateJWTSecret() + } + if !viper.IsSet("lasso.jwt.issuer") { + Cfg.JWT.Issuer = "Lasso" + } + if !viper.IsSet("lasso.jwt.maxAge") { + Cfg.JWT.MaxAge = 240 + } + if !viper.IsSet("lasso.jwt.compress") { + Cfg.JWT.Compress = true + } + + // cookie defaults + if !viper.IsSet("lasso.cookie.name") { + Cfg.Cookie.Name = "LassoCookie" + } + if !viper.IsSet("lasso.cookie.secure") { + Cfg.Cookie.Secure = false + } + if !viper.IsSet("lasso.cookie.httpOnly") { + Cfg.Cookie.HTTPOnly = true + } + + // headers defaults + if !viper.IsSet("lasso.headers.jwt") { + Cfg.Headers.JWT = "X-Lasso-Token" + } + if !viper.IsSet("lasso.headers.querystring") { + Cfg.Headers.QueryString = "access_token" + } + if !viper.IsSet("lasso.headers.redirect") { + Cfg.Headers.Redirect = "X-Lasso-Requested-URI" + } + if !viper.IsSet("lasso.headers.user") { + Cfg.Headers.User = "X-Lasso-User" + } + if !viper.IsSet("lasso.headers.success") { + Cfg.Headers.Success = "X-Lasso-Success" + } + + // db defaults + if !viper.IsSet("lasso.db.file") { + Cfg.DB.File = "data/lasso_bolt.db" + } + + // session HERE + if !viper.IsSet("lasso.session.name") { + Cfg.Session.Name = "lassoSession" + } + + // testing convenience variable + if !viper.IsSet("lasso.testing") { + Cfg.Testing = false + } + if !viper.IsSet("lasso.test_url") { + Cfg.TestURL = "" + } + // TODO: proably change this name, maybe set the domain/port the webapp runs on + if !viper.IsSet("lasso.webapp") { + Cfg.WebApp = false + } + + // OAuth defaults and client configuration + err := UnmarshalKey("oauth", &GenOAuth) + if err == nil { + if GenOAuth.Provider == Providers.Google { + setDefaultsGoogle() + // setDefaultsGoogle also configures the OAuthClient + } else if GenOAuth.Provider == Providers.GitHub { + setDefaultsGitHub() + configureOAuthClient() + } else { + configureOAuthClient() + } + } +} + +func setDefaultsGoogle() { + log.Info("configuring Google OAuth") + GenOAuth.UserInfoURL = "https://www.googleapis.com/oauth2/v3/userinfo" + OAuthClient = &oauth2.Config{ + ClientID: GenOAuth.ClientID, + ClientSecret: GenOAuth.ClientSecret, + Scopes: []string{ + // You have to select a scope from + // https://developers.google.com/identity/protocols/googlescopes#google_sign-in + "https://www.googleapis.com/auth/userinfo.email", + }, + Endpoint: google.Endpoint, + } + log.Infof("setting Google OAuth preferred login domain param 'hd' to %s", GenOAuth.PreferredDomain) + OAuthopts = oauth2.SetAuthURLParam("hd", GenOAuth.PreferredDomain) +} + +func setDefaultsGitHub() { + // log.Info("configuring GitHub OAuth") + if GenOAuth.AuthURL == "" { + GenOAuth.AuthURL = github.Endpoint.AuthURL + } + if GenOAuth.TokenURL == "" { + GenOAuth.TokenURL = github.Endpoint.TokenURL + } + if GenOAuth.UserInfoURL == "" { + GenOAuth.UserInfoURL = "https://api.github.com/user?access_token=" + } + if len(GenOAuth.Scopes) == 0 { + GenOAuth.Scopes = []string{"user"} + } +} + +func configureOAuthClient() { + log.Infof("configuring %s OAuth with Endpoint %s", GenOAuth.Provider, GenOAuth.AuthURL) + OAuthClient = &oauth2.Config{ + ClientID: GenOAuth.ClientID, + ClientSecret: GenOAuth.ClientSecret, + Endpoint: oauth2.Endpoint{ + AuthURL: GenOAuth.AuthURL, + TokenURL: GenOAuth.TokenURL, + }, + RedirectURL: GenOAuth.RedirectURL, + Scopes: GenOAuth.Scopes, + } +} + +var secretFile = os.Getenv("LASSO_ROOT") + "config/secret" + +// a-z A-Z 0-9 except no l, o, O +const charRunes = "abcdefghijkmnpqrstuvwxyzABCDEFGHIJKLMNPQRSTUVWXYZ012346789" + +const secretLen = 18 + +func getOrGenerateJWTSecret() string { + b, err := ioutil.ReadFile(secretFile) + if err == nil { + log.Info("jwt.secret read from " + secretFile) + } else { + // then generate a new secret and store it in the file + log.Debug(err) + log.Info("jwt.secret not found in " + secretFile) + log.Warn("generating new jwt.secret and storing it in " + secretFile) + + rand.Seed(time.Now().UnixNano()) + b := make([]byte, secretLen) + for i := range b { + b[i] = charRunes[rand.Intn(len(charRunes))] + } + err := ioutil.WriteFile(secretFile, b, 0600) + if err != nil { + log.Debug(err) + } + } + return string(b) +} diff --git a/pkg/jwtmanager/jwtmanager.go b/pkg/jwtmanager/jwtmanager.go index 04126902..2557706e 100644 --- a/pkg/jwtmanager/jwtmanager.go +++ b/pkg/jwtmanager/jwtmanager.go @@ -38,6 +38,9 @@ func init() { } Sites = make([]string, 0) + // TODO: the Sites that end up in the JWT come from here + // if we add fine grain ability (ACL?) to the equation + // then we're going to have to add something fancier here for i := 0; i < len(cfg.Cfg.Domains); i++ { Sites = append(Sites, cfg.Cfg.Domains[i]) } @@ -129,7 +132,7 @@ func ParseTokenString(tokenString string) (*jwt.Token, error) { func SiteInClaims(site string, claims *LassoClaims) bool { for _, s := range claims.Sites { if strings.Contains(site, s) { - log.Debugf("evaluating %s contains %s", site, s) + log.Debugf("site %s is found for claims.Site %s", site, s) return true } } diff --git a/pkg/model/model.go b/pkg/model/model.go index 6dd8e2b4..d5c3eef3 100644 --- a/pkg/model/model.go +++ b/pkg/model/model.go @@ -25,6 +25,8 @@ var ( //Db holds the db Db *bolt.DB + dbpath string + userBucket = []byte("users") teamBucket = []byte("teams") siteBucket = []byte("sites") @@ -32,11 +34,12 @@ var ( // may want to use encode/gob to store the user record func init() { - Db, _ = Open(os.Getenv("LASSO_ROOT") + cfg.Cfg.DB.File) + dbpath = os.Getenv("LASSO_ROOT") + cfg.Cfg.DB.File + Db, _ = OpenDB(dbpath) } -// Open the boltdb -func Open(dbfile string) (*bolt.DB, error) { +// OpenDB the boltdb +func OpenDB(dbfile string) (*bolt.DB, error) { opts := &bolt.Options{ Timeout: 50 * time.Millisecond, @@ -54,7 +57,9 @@ func Open(dbfile string) (*bolt.DB, error) { func getBucket(tx *bolt.Tx, key []byte) *bolt.Bucket { b, err := tx.CreateBucketIfNotExists(key) if err != nil { - log.Errorf("could not create bucket %s", err) + log.Errorf("could not create bucket in db %s", err) + log.Errorf("check the dbfile permissions at %s", dbpath) + log.Errorf("if there's really something wrong with the data ./do.sh includes a utility to browse the dbfile") return nil } return b diff --git a/pkg/model/model_test.go b/pkg/model/model_test.go index dab3fe1b..f2f83d00 100644 --- a/pkg/model/model_test.go +++ b/pkg/model/model_test.go @@ -17,14 +17,14 @@ import ( var testdb = "/tmp/storage-test.db" func init() { - Db, _ = Open(testdb) + Db, _ = OpenDB(testdb) log.SetLevel(log.DebugLevel) } func TestPutUserGetUser(t *testing.T) { os.Remove(testdb) - Open(testdb) + OpenDB(testdb) u1 := structs.User{ Email: "test@testing.com", @@ -58,7 +58,7 @@ func TestPutUserGetUser(t *testing.T) { func TestPutSiteGetSite(t *testing.T) { os.Remove(testdb) - Open(testdb) + OpenDB(testdb) s1 := structs.Site{Domain: "test.bnf.net"} s2 := &structs.Site{} @@ -73,7 +73,7 @@ func TestPutSiteGetSite(t *testing.T) { func TestPutTeamGetTeamDeleteTeam(t *testing.T) { os.Remove(testdb) - Open(testdb) + OpenDB(testdb) t1 := structs.Team{Name: "testname"} t2 := &structs.Team{} diff --git a/pkg/structs/structs.go b/pkg/structs/structs.go index 0e59f97d..62ab2481 100644 --- a/pkg/structs/structs.go +++ b/pkg/structs/structs.go @@ -7,9 +7,12 @@ type UserI interface { // User is inherited. type User struct { + // TODO: set Provider here so that we can pass it to db + // populated by db (via mapstructure) or from provider (via json) + // Provider string `json:"provider",mapstructure:"provider"` Username string `json:"username",mapstructure:"username"` - Name string `json:"name"` - Email string `json:"email"` + Name string `json:"name",mapstructure:"name"` + Email string `json:"email",mapstructure:"email"` CreatedOn int64 `json:"createdon"` LastUpdate int64 `json:"lastupdate"` ID int `json:"id",mapstructure:"id"` @@ -23,6 +26,9 @@ func (u *User) PrepareUserData() { // GoogleUser is a retrieved and authentiacted user from Google. // unused! + +// TODO: see if these should be pointers to the *User object as per +// https://golang.org/doc/effective_go.html#embedding type GoogleUser struct { User Sub string `json:"sub"` @@ -41,8 +47,8 @@ func (u *GoogleUser) PrepareUserData() { u.Username = u.Email } -// GithubUser is a retrieved and authentiacted user from Github. -type GithubUser struct { +// GitHubUser is a retrieved and authentiacted user from GitHub. +type GitHubUser struct { User Login string `json:"login"` Picture string `json:"avatar_url"` @@ -50,34 +56,20 @@ type GithubUser struct { } // PrepareUserData implement PersonalData interface -func (u *GithubUser) PrepareUserData() { +func (u *GitHubUser) PrepareUserData() { // always use the u.Login as the u.Username u.Username = u.Login } type IndieAuthUser struct { User - URL string `json:"me"` + URL string `json:"me"` } func (u *IndieAuthUser) PrepareUserData() { u.Username = u.URL } -// GenericOauth provides endoint for access -type GenericOauth struct { - ClientID string `mapstructure:"client_id"` - ClientSecret string `mapstructure:"client_secret"` - AuthURL string `mapstructure:"auth_url"` - TokenURL string `mapstructure:"token_url"` - RedirectURL string `mapstructure:"callback_url"` - RedirectURLs []string `mapstructure:"callback_urls"` - Scopes []string `mapstructure:"scopes"` - UserInfoURL string `mapstructure:"user_info_url"` - Provider string `mapstructure:"provider"` - PreferredDomain string `mapstructre:"preferredDomain"` -} - // Team has members and provides acess to sites type Team struct { Name string `json:"name",mapstructure:"name"` From 82cd35f72b561bed0dd8f31d315afa639c22456a Mon Sep 17 00:00:00 2001 From: Benjamin Foote Date: Thu, 11 Oct 2018 16:33:27 -0700 Subject: [PATCH 047/360] #23 email --> username --- pkg/jwtmanager/jwtmanager_test.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pkg/jwtmanager/jwtmanager_test.go b/pkg/jwtmanager/jwtmanager_test.go index b9211cd7..619a2152 100644 --- a/pkg/jwtmanager/jwtmanager_test.go +++ b/pkg/jwtmanager/jwtmanager_test.go @@ -5,6 +5,7 @@ import ( "github.com/LassoProject/lasso/pkg/cfg" "github.com/LassoProject/lasso/pkg/structs" + // log "github.com/Sirupsen/logrus" log "github.com/Sirupsen/logrus" "github.com/stretchr/testify/assert" @@ -12,7 +13,7 @@ import ( var ( u1 = structs.User{ - Email: "test@testing.com", + Username: "test@testing.com", EmailVerified: true, Name: "Test Name", } @@ -24,13 +25,13 @@ func init() { // log.SetLevel(log.DebugLevel) lc = LassoClaims{ - u1.Email, + u1.Username, Sites, StandardClaims, } } -func TestCreateUserTokenStringAndParseToEmail(t *testing.T) { +func TestCreateUserTokenStringAndParseToUsername(t *testing.T) { uts := CreateUserTokenString(u1) assert.NotEmpty(t, uts) @@ -40,8 +41,8 @@ func TestCreateUserTokenStringAndParseToEmail(t *testing.T) { t.Error(err) } else { log.Debugf("test parsed token string %v", utsParsed) - ptemail, _ := PTokenToEmail(utsParsed) - assert.Equal(t, u1.Email, ptemail) + ptUsername, _ := PTokenToUsername(utsParsed) + assert.Equal(t, u1.Username, ptUsername) } } From 2a8b006211ec733a7a8130573d1c93b5f646e1ce Mon Sep 17 00:00:00 2001 From: Benjamin Foote Date: Thu, 11 Oct 2018 16:34:02 -0700 Subject: [PATCH 048/360] fix #17 provid example configs and better explanation for config items --- config/config.yml_example | 51 +++++++++++++-------- config/config.yml_example_github | 31 +++++++++++++ config/config.yml_example_github_enterprise | 31 +++++++++++++ config/config.yml_example_google | 20 ++++++++ config/config.yml_example_indieauth | 24 ++++++++++ config/config.yml_example_oidc | 31 +++++++++++++ 6 files changed, 170 insertions(+), 18 deletions(-) create mode 100644 config/config.yml_example_github create mode 100644 config/config.yml_example_github_enterprise create mode 100644 config/config.yml_example_google create mode 100644 config/config.yml_example_indieauth create mode 100644 config/config.yml_example_oidc diff --git a/config/config.yml_example b/config/config.yml_example index 06dddcb9..7c22a388 100644 --- a/config/config.yml_example +++ b/config/config.yml_example @@ -5,14 +5,6 @@ lasso: listen: 0.0.0.0 port: 9090 - # set allowAllUsers: true to use Lasso to just accept anyone who can authenticate at the configured provider - allowAllUsers: false - - # Setting publicAccess: true will accept all requests, even without a cookie. - # If the user is logged in, the cookie will be validated and the user header will be set. - # You will need to direct people to the Lasso login page from your application. - publicAccess: false - # each of these domains must serve the url https://lasso.$domains[0] https://lasso.$domains[1] ... # so that the cookie which stores the JWT can be set in the relevant domain # usually you'll just have one. @@ -21,22 +13,39 @@ lasso: - yourdomain.com - yourotherdomain.com + # set allowAllUsers: true to use Lasso to just accept anyone who can authenticate at the configured provider + allowAllUsers: false + + # Setting publicAccess: true will accept all requests, even without a cookie. + # If the user is logged in, the cookie will be validated and the user header will be set. + # You will need to direct people to the Lasso login page from your application. + publicAccess: false + jwt: + # secret: a random 18 character string used to cryptographically sign the jwt + # if the secret is not set here then.. + # look for the secret in `./config/secret` + # if `./config/secret` doesn't exist then randomly generate a secret and store it there + # in order to run multiple instances of lasso on multiple servers (perhaps purely for validating the jwt), + # you'll want them all to have the same secret + secret: your_random_string issuer: Lasso + # number of seconds until jwt expires maxAge: 240 - secret: your_random_string + # compress the jwt compress: true cookie: # name of cookie to store the jwt - name: Lasso-cookie + name: LassoCookie # optionally force the domain of the cookie to set # domain: yourdomain.com secure: true httpOnly: true session: - name: lasso-session + # just the name of session variable stored locally + name: lassoSession headers: jwt: X-Lasso-Token @@ -46,13 +55,18 @@ lasso: db: file: data/lasso_bolt.db + # testing: force all 302 redirects to be rendered as a webpage with a link + testing: true + # test_url: add this URL to the page which lasso displays test_url: http://yourdomain.com + # webapp: WIP for web interface to lasso (mostly logs) + webapp: true # -# OAuth Provider Config +# OAuth Provider +# configure ONLY ONE of the following oauth providers # oauth: - # configure only one of the following # Google provider: google @@ -70,13 +84,14 @@ oauth: provider: github client_id: client_secret: - auth_url: https://github.com/login/oauth/authorize - token_url: https://github.com/login/oauth/access_token # callback_url is configured at github.com when setting up the app # set to e.g. https://lasso.yourdomain.com/auth - scopes: - - user - user_info_url: https://api.github.com/user?access_token= + # defaults (uncomment and change these if you are using github enterprise on-prem) + # auth_url: https://github.com/login/oauth/authorize + # token_url: https://github.com/login/oauth/access_token + # user_info_url: https://api.github.com/user?access_token= + # scopes: + # - user # Generic OpenID Connect provider: oidc diff --git a/config/config.yml_example_github b/config/config.yml_example_github new file mode 100644 index 00000000..9924450f --- /dev/null +++ b/config/config.yml_example_github @@ -0,0 +1,31 @@ + +# lasso config +# bare minimum to get lasso running with github + +lasso: + # domains: + # valid domains that the jwt cookies can be set into + # the callback_urls will be to these domains + # for github that's only one domain since they only allow one callback URL + # https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps/#redirect-urls + # each of these domains must serve the url https://login.$domains[0] https://login.$domains[1] ... + domains: + - yourothersite.io + + # set allowAllUsers: true to use Lasso to just accept anyone who can authenticate at GitHub + # allowAllUsers: true + +oauth: + # create a new OAuth application at: + # https://github.com/settings/applications/new + provider: github + client_id: xxxxxxxxxxxxxxxxxxxx + client_secret: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + + # these GitHub OAuth defaults are set for you.. + # from https://godoc.org/golang.org/x/oauth2/github + # auth_url: https://github.com/login/oauth/authorize + # token_url: https://github.com/login/oauth/access_token + # scopes: + # - user + # user_info_url: https://api.github.com/user?access_token= \ No newline at end of file diff --git a/config/config.yml_example_github_enterprise b/config/config.yml_example_github_enterprise new file mode 100644 index 00000000..3d1eec6e --- /dev/null +++ b/config/config.yml_example_github_enterprise @@ -0,0 +1,31 @@ +# lasso config +# bare minimum to get lasso running with github enterprise +# see config.yml_example for all options + +lasso: + # domains: + # valid domains that the jwt cookies can be set into + # each of these domains must serve the url https://login.$domains[0] https://login.$domains[1] ... + # the callback_urls will be to these domains + domains: + - yoursite.com + - yourothersite.io + + # - OR - + # instead of setting specific domains you may prefer to allow all users... + # set allowAllUsers: true to use Lasso to just accept anyone who can authenticate at the configured provider + # allowAllUsers: true + +oauth: + # create a new OAuth application at: + # https://githubenterprise.yoursite.com/settings/applications/new + provider: github + client_id: xxxxxxxxxxxxxxxxxxxx + client_secret: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + auth_url: https://githubenterprise.yoursite.com/login/oauth/authorize + token_url: https://githubenterprise.yoursite.com/login/oauth/access_token + user_info_url: https://githubenterprise.yoursite.com/user?access_token= + + # these GitHub OAuth defaults are set for you.. + # scopes: + # - user diff --git a/config/config.yml_example_google b/config/config.yml_example_google new file mode 100644 index 00000000..57770053 --- /dev/null +++ b/config/config.yml_example_google @@ -0,0 +1,20 @@ + +# lasso config +# bare minimum to get lasso running with google + +lasso: + domains: + - yourdomain.com + - yourotherdomain.com + +oauth: + provider: google + # get credentials from... + # https://console.developers.google.com/apis/credentials + client_id: xxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com + client_secret: xxxxxxxxxxxxxxxxxxxxxxxx + callback_urls: + - http://yourdomain.com:9090/auth + - http://yourotherdomain.com:9090/auth + preferredDomain: yourdomain.com + # endpoints set from https://godoc.org/golang.org/x/oauth2/google diff --git a/config/config.yml_example_indieauth b/config/config.yml_example_indieauth new file mode 100644 index 00000000..6c4fb678 --- /dev/null +++ b/config/config.yml_example_indieauth @@ -0,0 +1,24 @@ + +# lasso config +# bare minimum to get lasso running with IndieAuth + +lasso: + # domains: + # valid domains that the jwt cookies can be set into + # the callback_urls will be to these domains + domains: + - yourdomain.com + + # set allowAllUsers: true to use Lasso to just accept anyone who can authenticate at the configured provider + allowAllUsers: true + + # Setting publicAccess: true will accept all requests, even without a cookie. + publicAccess: true + +oauth: + # IndieAuth + # https://indielogin.com/api + provider: indieauth + client_id: http://yourdomain.com + auth_url: https://indielogin.com/auth + callback_url: http://lasso.yourdomain.com:9090/auth diff --git a/config/config.yml_example_oidc b/config/config.yml_example_oidc new file mode 100644 index 00000000..41be4c96 --- /dev/null +++ b/config/config.yml_example_oidc @@ -0,0 +1,31 @@ + +# lasso config +# bare minimum to get lasso running with OpenID Connect (such as okta) + +lasso: + # domains: + # valid domains that the jwt cookies can be set into + # the callback_urls will be to these domains + domains: + - yourdomain.com + - yourotherdomain.com + + # - OR - + # instead of setting specific domains you may prefer to allow all users... + # set allowAllUsers: true to use Lasso to just accept anyone who can authenticate at the configured provider + # allowAllUsers: true + +oauth: + # Generic OpenID Connect + # including okta + provider: oidc + client_id: xxxxxxxxxxxxxxxxxxxxxxxxxxxx + client_secret: xxxxxxxxxxxxxxxxxxxxxxxx + auth_url: https://{yourOktaDomain}/oauth2/default/v1/authorize + token_url: https://{yourOktaDomain}/oauth2/default/v1/token + user_info_url: https://{yourOktaDomain}/oauth2/default/v1/userinfo + scopes: + - openid + - email + - profile + callback_url: http://lasso.yourdomain.com:9090/auth From 408bc8572e54acc9070891d2f4a39e52993a6872 Mon Sep 17 00:00:00 2001 From: Benjamin Foote Date: Thu, 11 Oct 2018 16:34:57 -0700 Subject: [PATCH 049/360] ignore local configs --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 02aec4ee..78910f1f 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,7 @@ main config/google_config.json .vscode/* lasso +config/config.yml_google +config/config.yml_github +config/secret +config/config.yml_orig From edeb05748c5ef6a3784945895a0893be2c34b962 Mon Sep 17 00:00:00 2001 From: Benjamin Foote Date: Fri, 12 Oct 2018 16:09:44 -0700 Subject: [PATCH 050/360] add whitelist capabilities --- Dockerfile | 2 +- config/config.yml_example | 4 ++++ handlers/handlers.go | 8 ++++++++ pkg/cfg/cfg.go | 7 ++++--- pkg/cfg/cfg_test.go | 2 +- 5 files changed, 18 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 8664a808..9a74f289 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -# bfoote/lasso +# lassoproject/lasso # https://github.com/LassoProject/lasso FROM golang:1.8 diff --git a/config/config.yml_example b/config/config.yml_example index 7c22a388..3e265b9c 100644 --- a/config/config.yml_example +++ b/config/config.yml_example @@ -21,6 +21,10 @@ lasso: # You will need to direct people to the Lasso login page from your application. publicAccess: false + TODO: + whiteList: + - + jwt: # secret: a random 18 character string used to cryptographically sign the jwt # if the secret is not set here then.. diff --git a/handlers/handlers.go b/handlers/handlers.go index 8b2aa8ad..d13c2cdf 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -331,6 +331,14 @@ func VerifyUser(u interface{}) (ok bool, err error) { ok = true log.Debugf("skipping verify user since cfg.Cfg.AllowAllUsers is %t", cfg.Cfg.AllowAllUsers) // if we're not allowing all users, and we have domains configured and this email isn't in one of those domains... + } else if len(cfg.Cfg.WhiteList) != 0 { + for _, wl := range cfg.Cfg.WhiteList { + if user.Username == wl { + log.Debugf("found user.Username in WhiteList: %s", user.Username) + ok = true + break + } + } } else if len(cfg.Cfg.Domains) != 0 && !domains.IsUnderManagement(user.Email) { err = fmt.Errorf("Email %s is not within a lasso managed domain", user.Email) // } else if !domains.IsUnderManagement(user.HostDomain) { diff --git a/pkg/cfg/cfg.go b/pkg/cfg/cfg.go index 8b9f4a5c..0d93cd2e 100644 --- a/pkg/cfg/cfg.go +++ b/pkg/cfg/cfg.go @@ -16,12 +16,13 @@ import ( "github.com/spf13/viper" ) -// CfgT lasso jwt cookie configuration -type CfgT struct { +// config lasso jwt cookie configuration +type config struct { LogLevel string `mapstructure:"logLevel"` Listen string `mapstructure:"listen"` Port int `mapstructure:"port"` Domains []string `mapstructure:"domains"` + WhiteList []string `mapstructure:"whitelist"` AllowAllUsers bool `mapstructure:"allowAllUsers"` PublicAccess bool `mapstructure:"publicAccess"` JWT struct { @@ -78,7 +79,7 @@ type OAuthProviders struct { var ( // Cfg the main exported config variable - Cfg CfgT + Cfg config // GenOAuth exported OAuth config variable // TODO: I think GenOAuth and OAuthConfig can be combined! diff --git a/pkg/cfg/cfg_test.go b/pkg/cfg/cfg_test.go index c8a21064..a19726b2 100644 --- a/pkg/cfg/cfg_test.go +++ b/pkg/cfg/cfg_test.go @@ -9,7 +9,7 @@ import ( ) var ( - cfg CfgT + cfg config ) func init() { From 12d43e8f3e7a947fc1808a310d7f963ef07ca4ae Mon Sep 17 00:00:00 2001 From: Benjamin Foote Date: Wed, 17 Oct 2018 14:36:08 -0700 Subject: [PATCH 051/360] record StatusCode for better loggin --- handlers/handlers.go | 57 +++++++++++++++++++++++----------------- pkg/context/context.go | 10 ------- pkg/response/response.go | 41 +++++++++++++++++++++++++++++ pkg/timelog/timelog.go | 29 +++++++++----------- 4 files changed, 86 insertions(+), 51 deletions(-) delete mode 100644 pkg/context/context.go create mode 100644 pkg/response/response.go diff --git a/handlers/handlers.go b/handlers/handlers.go index d13c2cdf..d6a18f43 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -2,7 +2,6 @@ package handlers import ( "bytes" - "context" "crypto/rand" "encoding/base64" "encoding/json" @@ -16,7 +15,6 @@ import ( log "github.com/Sirupsen/logrus" "github.com/LassoProject/lasso/pkg/cfg" - lctx "github.com/LassoProject/lasso/pkg/context" "github.com/LassoProject/lasso/pkg/cookie" "github.com/LassoProject/lasso/pkg/domains" "github.com/LassoProject/lasso/pkg/jwtmanager" @@ -135,33 +133,19 @@ func ClaimsFromJWT(jwt string) (jwtmanager.LassoClaims, error) { return claims, nil } -// the standard error -// this is captured by nginx, which converts the 401 into 302 to the login page -func error401(w http.ResponseWriter, r *http.Request, ae AuthError) { - log.Error(ae.Error) - cookie.ClearCookie(w, r) - context.WithValue(r.Context(), lctx.StatusCode, http.StatusUnauthorized) - // w.Header().Set("X-Lasso-Error", ae.Error) - http.Error(w, ae.Error, http.StatusUnauthorized) - // TODO put this back in place if multiple auth mechanism are available - // c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{"message": errStr}) -} - -func error401na(w http.ResponseWriter, r *http.Request) { - error401(w, r, AuthError{Error: "not authorized"}) -} - // ValidateRequestHandler /validate // TODO this should use the handler interface func ValidateRequestHandler(w http.ResponseWriter, r *http.Request) { log.Debug("/validate") + // TODO: collapse all of the `if !cfg.Cfg.PublicAccess` calls + // perhaps using an `ok=false` pattern jwt := FindJWT(r) // if jwt != "" { if jwt == "" { // If the module is configured to allow public access with no authentication, return 200 now if !cfg.Cfg.PublicAccess { - error401(w, r, AuthError{Error: "no jwt found in request for "}) + error401(w, r, AuthError{Error: "no jwt found in request"}) } else { w.Header().Add(cfg.Cfg.Headers.User, "") } @@ -178,6 +162,7 @@ func ValidateRequestHandler(w http.ResponseWriter, r *http.Request) { } return } + if claims.Username == "" { // no email in jwt if !cfg.Cfg.PublicAccess { @@ -204,7 +189,9 @@ func ValidateRequestHandler(w http.ResponseWriter, r *http.Request) { w.Header().Add(cfg.Cfg.Headers.User, claims.Username) w.Header().Add(cfg.Cfg.Headers.Success, "true") log.Debugf("response header "+cfg.Cfg.Headers.User+": %s", w.Header().Get(cfg.Cfg.Headers.User)) - renderIndex(w, "/validate user found in jwt "+claims.Username) + + // good to go!! + ok200(w, r) // TODO // parse the jwt and see if the claim is valid for the domain @@ -305,7 +292,6 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) { // bounce to oauth provider for login var lURL = loginURL(r, state) log.Debugf("redirecting to oauthURL %s", lURL) - context.WithValue(r.Context(), lctx.StatusCode, 302) redirect302(w, r, lURL) } } @@ -405,8 +391,6 @@ func CallbackHandler(w http.ResponseWriter, r *http.Request) { session.Values[requestedURL] = 0 session.Save(r, w) - // and redirect - context.WithValue(r.Context(), lctx.StatusCode, 302) redirect302(w, r, requestedURL) return } @@ -566,13 +550,38 @@ func getUserInfoFromIndieAuth(r *http.Request, user *structs.User) error { return nil } +// the standard error +// this is captured by nginx, which converts the 401 into 302 to the login page +func error401(w http.ResponseWriter, r *http.Request, ae AuthError) { + log.Error(ae.Error) + cookie.ClearCookie(w, r) + // w.Header().Set("X-Lasso-Error", ae.Error) + http.Error(w, ae.Error, http.StatusUnauthorized) + // TODO put this back in place if multiple auth mechanism are available + // c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{"message": errStr}) +} + +func error401na(w http.ResponseWriter, r *http.Request) { + error401(w, r, AuthError{Error: "not authorized"}) +} + func redirect302(w http.ResponseWriter, r *http.Request, rURL string) { if cfg.Cfg.Testing { var tmp = cfg.Cfg.TestURL cfg.Cfg.TestURL = rURL + // TODO: allow template to take an array of URLs and just push to those renderIndex(w, "302 redirect to: "+cfg.Cfg.TestURL) cfg.Cfg.TestURL = tmp return } - http.Redirect(w, r, rURL, 302) + http.Redirect(w, r, rURL, http.StatusFound) +} + +func ok200(w http.ResponseWriter, r *http.Request) { + + n, err := w.Write(nil) + if err != nil { + log.Error(err) + } + log.Debugf("ok200 with empty body (bytes %d)", n) } diff --git a/pkg/context/context.go b/pkg/context/context.go deleted file mode 100644 index 3cc719bb..00000000 --- a/pkg/context/context.go +++ /dev/null @@ -1,10 +0,0 @@ -package context - -// Key named keys for context map -type Key string - -func (c Key) String() string { - return "mypackage context key " + string(c) -} - -var StatusCode = Key("statusCode") diff --git a/pkg/response/response.go b/pkg/response/response.go new file mode 100644 index 00000000..ff7bfd8c --- /dev/null +++ b/pkg/response/response.go @@ -0,0 +1,41 @@ +package response + +import "net/http" +import log "github.com/Sirupsen/logrus" + +// we wrap ResponseWriter so that we can store the StatusCode +// and then pull it out later for logging +// https://play.golang.org/p/wPHaX9DH-Ik + +// CaptureWriter extends http.ResponseWriter +type CaptureWriter struct { + http.ResponseWriter + StatusCode int +} + +func (w *CaptureWriter) Write(b []byte) (int, error) { + if w.StatusCode == 0 { + w.StatusCode = 200 + log.Debugf("set w.StatusCode %d", w.StatusCode) + } + log.Debugf("CaptureWriter.Write code %d", w.StatusCode) + return w.ResponseWriter.Write(b) +} + +// Header calls http.Writer.Header() +func (w *CaptureWriter) Header() http.Header { + log.Debugf("CaptureWriter.Header code %d", w.StatusCode) + return w.ResponseWriter.Header() +} + +// WriteHeader calls http.Writer.WriteHeader(code) +func (w *CaptureWriter) WriteHeader(code int) { + w.StatusCode = code + log.Debugf("CaptureWriter.WriteHeader code %d", w.StatusCode) + w.ResponseWriter.WriteHeader(code) +} + +// GetStatusCode return w.StatusCode +func (w *CaptureWriter) GetStatusCode() int { + return w.StatusCode +} diff --git a/pkg/timelog/timelog.go b/pkg/timelog/timelog.go index 810e82ee..6cde0c29 100644 --- a/pkg/timelog/timelog.go +++ b/pkg/timelog/timelog.go @@ -5,7 +5,7 @@ import ( "net/http" "time" - lctx "github.com/LassoProject/lasso/pkg/context" + "github.com/LassoProject/lasso/pkg/response" log "github.com/Sirupsen/logrus" // "github.com/mattn/go-isatty" @@ -22,38 +22,33 @@ var ( reset = string([]byte{27, 91, 48, 109}) ) -// HERE you left off trying to figure out how to implement middleware in gorilla mux -func TimeLog(nextHandler http.Handler) http.HandlerFunc { +// TimeLog records how long it takes to process the http request and produce the response (latency) +func TimeLog(nextHandler http.Handler) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { - log.Debugf("Request received : %v\n", r) + log.Debugf("Request received : %v", r) start := time.Now() // make the call + v := response.CaptureWriter{w, 0} ctx := context.Background() - nextHandler.ServeHTTP(w, r.WithContext(ctx)) + nextHandler.ServeHTTP(&v, r.WithContext(ctx)) // Stop timer end := time.Now() - log.Debug("Request handled successfully") latency := end.Sub(start) - clientIP := r.RemoteAddr - method := r.Method - // var statusCode int - // var statusColor string - statusCode := ctx.Value(lctx.StatusCode) - // TODO: this just doesn't seem to work, how can we get the statusCode from the context? - // log.Debugf("statuscode: %v", statusCode) - if statusCode == nil { - statusCode = 200 - } - statusColor := colorForStatus(statusCode.(int)) + log.Debugf("Request handled successfully: %v", v.GetStatusCode()) + var statusCode = v.GetStatusCode() + statusColor := colorForStatus(statusCode) path := r.URL.Path host := r.Host referer := r.Header.Get("Referer") + clientIP := r.RemoteAddr + method := r.Method + log.Infof("|%s %3d %s| %13v | %s | %s %s %s | %s", statusColor, statusCode, reset, latency, From e40999c7a95ee68705fd848ac576de2046486650 Mon Sep 17 00:00:00 2001 From: Benjamin Foote Date: Wed, 17 Oct 2018 15:43:25 -0700 Subject: [PATCH 052/360] record request number and average latency --- pkg/timelog/timelog.go | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/pkg/timelog/timelog.go b/pkg/timelog/timelog.go index 6cde0c29..5185166a 100644 --- a/pkg/timelog/timelog.go +++ b/pkg/timelog/timelog.go @@ -12,14 +12,16 @@ import ( ) var ( - green = string([]byte{27, 91, 57, 55, 59, 52, 50, 109}) - white = string([]byte{27, 91, 57, 48, 59, 52, 55, 109}) - yellow = string([]byte{27, 91, 57, 55, 59, 52, 51, 109}) - red = string([]byte{27, 91, 57, 55, 59, 52, 49, 109}) - blue = string([]byte{27, 91, 57, 55, 59, 52, 52, 109}) - magenta = string([]byte{27, 91, 57, 55, 59, 52, 53, 109}) - cyan = string([]byte{27, 91, 57, 55, 59, 52, 54, 109}) - reset = string([]byte{27, 91, 48, 109}) + green = string([]byte{27, 91, 57, 55, 59, 52, 50, 109}) + white = string([]byte{27, 91, 57, 48, 59, 52, 55, 109}) + yellow = string([]byte{27, 91, 57, 55, 59, 52, 51, 109}) + red = string([]byte{27, 91, 57, 55, 59, 52, 49, 109}) + blue = string([]byte{27, 91, 57, 55, 59, 52, 52, 109}) + magenta = string([]byte{27, 91, 57, 55, 59, 52, 53, 109}) + cyan = string([]byte{27, 91, 57, 55, 59, 52, 54, 109}) + reset = string([]byte{27, 91, 48, 109}) + req = int64(0) + avgLatency = int64(0) ) // TimeLog records how long it takes to process the http request and produce the response (latency) @@ -35,9 +37,9 @@ func TimeLog(nextHandler http.Handler) func(http.ResponseWriter, *http.Request) // Stop timer end := time.Now() - latency := end.Sub(start) - + req++ + avgLatency = avgLatency + ((int64(latency) - avgLatency) / req) log.Debugf("Request handled successfully: %v", v.GetStatusCode()) var statusCode = v.GetStatusCode() statusColor := colorForStatus(statusCode) @@ -49,9 +51,9 @@ func TimeLog(nextHandler http.Handler) func(http.ResponseWriter, *http.Request) clientIP := r.RemoteAddr method := r.Method - log.Infof("|%s %3d %s| %13v | %s | %s %s %s | %s", + log.Infof("|%s %3d %s| %d %10v %10v | %s | %s %s %s | %s", statusColor, statusCode, reset, - latency, + req, latency, time.Duration(avgLatency), clientIP, method, host, path, referer) From 5af4dd55c8da3201d1238c0eff0acc515c3dc0e6 Mon Sep 17 00:00:00 2001 From: Benjamin Foote Date: Wed, 17 Oct 2018 16:26:03 -0700 Subject: [PATCH 053/360] append to TestURLs to capture full round trip of 302 redirects --- handlers/handlers.go | 13 +++++-------- pkg/cfg/cfg.go | 19 ++++++++++++------- templates/index.tmpl | 6 +++++- 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/handlers/handlers.go b/handlers/handlers.go index d6a18f43..adfcc1f6 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -27,8 +27,8 @@ import ( // Index variables passed to index.tmpl // TODO: turn TestURL into an array of URLs to display type Index struct { - Msg string - TestURL string + Msg string + TestURLs []string } // AuthError sets the values to return to nginx @@ -297,7 +297,7 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) { } func renderIndex(w http.ResponseWriter, msg string) { - if err := indexTemplate.Execute(w, &Index{Msg: msg, TestURL: cfg.Cfg.TestURL}); err != nil { + if err := indexTemplate.Execute(w, &Index{Msg: msg, TestURLs: cfg.Cfg.TestURLs}); err != nil { log.Error(err) } } @@ -567,11 +567,8 @@ func error401na(w http.ResponseWriter, r *http.Request) { func redirect302(w http.ResponseWriter, r *http.Request, rURL string) { if cfg.Cfg.Testing { - var tmp = cfg.Cfg.TestURL - cfg.Cfg.TestURL = rURL - // TODO: allow template to take an array of URLs and just push to those - renderIndex(w, "302 redirect to: "+cfg.Cfg.TestURL) - cfg.Cfg.TestURL = tmp + cfg.Cfg.TestURLs = append(cfg.Cfg.TestURLs, rURL) + renderIndex(w, "302 redirect to: "+rURL) return } http.Redirect(w, r, rURL, http.StatusFound) diff --git a/pkg/cfg/cfg.go b/pkg/cfg/cfg.go index 0d93cd2e..6b4c955c 100644 --- a/pkg/cfg/cfg.go +++ b/pkg/cfg/cfg.go @@ -50,9 +50,10 @@ type config struct { Session struct { Name string `mapstructure:"name"` } - TestURL string `mapstructure:"test_url"` - Testing bool `mapstructure:"testing"` - WebApp bool `mapstructure:"webapp"` + TestURL string `mapstructure:"test_url"` + TestURLs []string `mapstructure:"test_urls"` + Testing bool `mapstructure:"testing"` + WebApp bool `mapstructure:"webapp"` } // oauth config items endoint for access @@ -246,8 +247,8 @@ func setDefaults() { if !viper.IsSet("lasso.testing") { Cfg.Testing = false } - if !viper.IsSet("lasso.test_url") { - Cfg.TestURL = "" + if viper.IsSet("lasso.test_url") { + Cfg.TestURLs = append(Cfg.TestURLs, Cfg.TestURL) } // TODO: proably change this name, maybe set the domain/port the webapp runs on if !viper.IsSet("lasso.webapp") { @@ -282,8 +283,12 @@ func setDefaultsGoogle() { }, Endpoint: google.Endpoint, } - log.Infof("setting Google OAuth preferred login domain param 'hd' to %s", GenOAuth.PreferredDomain) - OAuthopts = oauth2.SetAuthURLParam("hd", GenOAuth.PreferredDomain) + if GenOAuth.PreferredDomain != "" { + log.Infof("setting Google OAuth preferred login domain param 'hd' to %s", GenOAuth.PreferredDomain) + OAuthopts = oauth2.SetAuthURLParam("hd", GenOAuth.PreferredDomain) + } else { + OAuthopts = oauth2.SetAuthURLParam("hd", "") + } } func setDefaultsGitHub() { diff --git a/templates/index.tmpl b/templates/index.tmpl index ecfcf43b..31ba1acc 100644 --- a/templates/index.tmpl +++ b/templates/index.tmpl @@ -13,7 +13,11 @@

  • login
  • logout
  • validate
  • -
  • {{ .TestURL }}
  • +{{ if .TestURLs }} + {{ range $url := .TestURLs}} +
  • {{ $url }}
  • + {{ end }} +{{ end }} For support, please contact your network administrator or whomever setup nginx to use Lasso. From 2d186319af9813bc5f883919e50ffd9dfffdf797 Mon Sep 17 00:00:00 2001 From: Aaron Parecki Date: Fri, 19 Oct 2018 10:31:57 -0700 Subject: [PATCH 054/360] use Provider from cfg Co-Authored-By: bnfinet --- handlers/handlers.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/handlers/handlers.go b/handlers/handlers.go index 8b2aa8ad..937b8cd8 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -411,7 +411,7 @@ func CallbackHandler(w http.ResponseWriter, r *http.Request) { func getUserInfo(r *http.Request, user *structs.User) error { // indieauth sends the "me" setting in json back to the callback, so just pluck it from the callback - if cfg.GenOAuth.Provider == "indieauth" { + if cfg.GenOAuth.Provider == cfg.Providers.IndieAuth { return getUserInfoFromIndieAuth(r, user) } From f6b3d5af98ce4a7215ea2c251a1c11f08240ceb8 Mon Sep 17 00:00:00 2001 From: Benjamin Foote Date: Fri, 19 Oct 2018 10:50:24 -0700 Subject: [PATCH 055/360] don't log in color if it's not a tty, structured logs, general cleanup --- handlers/handlers.go | 7 +++--- main.go | 8 +++--- pkg/cfg/cfg.go | 5 ++-- pkg/cookie/cookie.go | 8 ++++-- pkg/timelog/timelog.go | 45 ++++++++++++++++++++++++++++------ pkg/transciever/transciever.go | 4 +-- 6 files changed, 56 insertions(+), 21 deletions(-) diff --git a/handlers/handlers.go b/handlers/handlers.go index adfcc1f6..2e0059c3 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -25,7 +25,6 @@ import ( ) // Index variables passed to index.tmpl -// TODO: turn TestURL into an array of URLs to display type Index struct { Msg string TestURLs []string @@ -172,7 +171,9 @@ func ValidateRequestHandler(w http.ResponseWriter, r *http.Request) { } return } - log.Infof("username from jwt cookie: %s", claims.Username) + log.WithFields(log.Fields{ + "username": claims.Username, + }).Info("jwt cookie") if !cfg.Cfg.AllowAllUsers { if !jwtmanager.SiteInClaims(r.Host, &claims) { @@ -188,7 +189,7 @@ func ValidateRequestHandler(w http.ResponseWriter, r *http.Request) { // renderIndex(w, "user found from email "+user.Email) w.Header().Add(cfg.Cfg.Headers.User, claims.Username) w.Header().Add(cfg.Cfg.Headers.Success, "true") - log.Debugf("response header "+cfg.Cfg.Headers.User+": %s", w.Header().Get(cfg.Cfg.Headers.User)) + log.WithFields(log.Fields{cfg.Cfg.Headers.User: w.Header().Get(cfg.Cfg.Headers.User)}).Debug("response header") // good to go!! ok200(w, r) diff --git a/main.go b/main.go index 5330ab85..64ce5d3a 100644 --- a/main.go +++ b/main.go @@ -19,12 +19,9 @@ import ( func main() { log.Info("starting lasso") mux := http.NewServeMux() - // router := mux.NewRouter() - // router.HandleFunc("/", handlers.IndexHandler) authH := http.HandlerFunc(handlers.ValidateRequestHandler) mux.HandleFunc("/validate", timelog.TimeLog(authH)) - // mux.HandleFunc("/validate", handlers.ValidateRequestHandler) loginH := http.HandlerFunc(handlers.LoginHandler) mux.HandleFunc("/login", timelog.TimeLog(loginH)) @@ -35,7 +32,7 @@ func main() { callH := http.HandlerFunc(handlers.CallbackHandler) mux.HandleFunc("/auth", timelog.TimeLog(callH)) - // router.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("./static")))) + // serve static files from /static mux.Handle("/static", http.FileServer(http.Dir("./static"))) if cfg.Cfg.WebApp { @@ -57,6 +54,9 @@ func main() { // Good practice: enforce timeouts for servers you create! WriteTimeout: 15 * time.Second, ReadTimeout: 15 * time.Second, + /// logrus has an example of using ErrorLog but it doesn't apply to this MUX implimentation + // https://github.com/sirupsen/logrus#logger-as-an-iowriter + // ErrorLog: log.New(w, "", 0), } log.Fatal(srv.ListenAndServe()) diff --git a/pkg/cfg/cfg.go b/pkg/cfg/cfg.go index 6b4c955c..e7cc544a 100644 --- a/pkg/cfg/cfg.go +++ b/pkg/cfg/cfg.go @@ -138,7 +138,8 @@ func ParseConfig() { // log.Fatalf(err.prob) panic(errT) } - log.Debugf("secret: %s", string(Cfg.JWT.Secret)) + // don't log the secret! + // log.Debugf("secret: %s", string(Cfg.JWT.Secret)) } // UnmarshalKey populate struct from contents of cfg tree at key @@ -336,7 +337,7 @@ func getOrGenerateJWTSecret() string { // then generate a new secret and store it in the file log.Debug(err) log.Info("jwt.secret not found in " + secretFile) - log.Warn("generating new jwt.secret and storing it in " + secretFile) + log.Warn("generating random jwt.secret and storing it in " + secretFile) rand.Seed(time.Now().UnixNano()) b := make([]byte, secretLen) diff --git a/pkg/cookie/cookie.go b/pkg/cookie/cookie.go index a5ad22ee..885e575b 100644 --- a/pkg/cookie/cookie.go +++ b/pkg/cookie/cookie.go @@ -25,7 +25,7 @@ func setCookie(w http.ResponseWriter, r *http.Request, val string, maxAge int) { domain := domains.Matches(r.Host) // Allow overriding the cookie domain in the config file if cfg.Cfg.Cookie.Domain != "" { - domain = cfg.Cfg.Cookie.Domain + domain = cfg.Cfg.Cookie.Domain log.Debugf("setting the cookie domain to %v", domain) } // log.Debugf("cookie %s expires %d", cfg.Cfg.Cookie.Name, expires) @@ -49,7 +49,11 @@ func Cookie(r *http.Request) (string, error) { if cookie.Value == "" { return "", errors.New("Cookie token empty") } - log.Debugf("cookie %s: %s", cfg.Cfg.Cookie.Name, cookie.Value) + + log.WithFields(log.Fields{ + "cookieName": cfg.Cfg.Cookie.Name, + "cookieValue": cookie.Value, + }).Debug("cookie") return cookie.Value, err } diff --git a/pkg/timelog/timelog.go b/pkg/timelog/timelog.go index 5185166a..f1a95952 100644 --- a/pkg/timelog/timelog.go +++ b/pkg/timelog/timelog.go @@ -2,16 +2,20 @@ package timelog import ( "context" + "fmt" "net/http" + "os" + "strconv" "time" "github.com/LassoProject/lasso/pkg/response" log "github.com/Sirupsen/logrus" - // "github.com/mattn/go-isatty" + isatty "github.com/mattn/go-isatty" ) var ( + useColor = false green = string([]byte{27, 91, 57, 55, 59, 52, 50, 109}) white = string([]byte{27, 91, 57, 48, 59, 52, 55, 109}) yellow = string([]byte{27, 91, 57, 55, 59, 52, 51, 109}) @@ -24,6 +28,13 @@ var ( avgLatency = int64(0) ) +func init() { + if isatty.IsTerminal(os.Stdout.Fd()) { + useColor = true + } + // useColor = false +} + // TimeLog records how long it takes to process the http request and produce the response (latency) func TimeLog(nextHandler http.Handler) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { @@ -42,7 +53,6 @@ func TimeLog(nextHandler http.Handler) func(http.ResponseWriter, *http.Request) avgLatency = avgLatency + ((int64(latency) - avgLatency) / req) log.Debugf("Request handled successfully: %v", v.GetStatusCode()) var statusCode = v.GetStatusCode() - statusColor := colorForStatus(statusCode) path := r.URL.Path host := r.Host @@ -51,12 +61,24 @@ func TimeLog(nextHandler http.Handler) func(http.ResponseWriter, *http.Request) clientIP := r.RemoteAddr method := r.Method - log.Infof("|%s %3d %s| %d %10v %10v | %s | %s %s %s | %s", - statusColor, statusCode, reset, - req, latency, time.Duration(avgLatency), - clientIP, - method, host, path, - referer) + log.WithFields(log.Fields{ + "statusCode": statusCode, + "request": req, + "latency": fmt.Sprintf("%10v", time.Duration(latency)), + "avgLatency": fmt.Sprintf("%10v", time.Duration(avgLatency)), + "ipPort": clientIP, + "method": method, + "host": host, + "path": path, + "referer": referer, + }).Infof("|%s| %10v %s", colorStatus(statusCode), time.Duration(latency), path) + + // log.Infof("|%s %3d %s| %d %10v %10v | %s | %s %s %s | %s", + // statusColor, statusCode, reset, + // req, latency, time.Duration(avgLatency), + // clientIP, + // method, host, path, + // referer) } } @@ -72,3 +94,10 @@ func colorForStatus(code int) string { return red } } + +func colorStatus(code int) string { + if !useColor { + return strconv.Itoa(code) + } + return fmt.Sprintf("%s %3d %s", colorForStatus(code), code, reset) +} diff --git a/pkg/transciever/transciever.go b/pkg/transciever/transciever.go index 9925636d..2eb2e3d9 100644 --- a/pkg/transciever/transciever.go +++ b/pkg/transciever/transciever.go @@ -27,7 +27,7 @@ func ExplicitInit() { } func (WS WSHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - log.Infof("ws endpoint") + log.Info("ws endpoint") // jwt := handlers.FindJWT(r) // if jwt == "" { // http.Error(w, "your mother", http.StatusUnauthorized) @@ -42,6 +42,6 @@ func (WS WSHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // http.Error(w, "your mother", http.StatusUnauthorized) // return // } - log.Info("hub %v", hh.Hub) + log.Infof("hub %v", hh.Hub) serveWs(hh.Hub, w, r) } From 288b91553658f184db550c79282edfa42ee94de2 Mon Sep 17 00:00:00 2001 From: Benjamin Foote Date: Fri, 19 Oct 2018 10:54:58 -0700 Subject: [PATCH 056/360] even simpler! --- config/config.yml_example_github | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/config/config.yml_example_github b/config/config.yml_example_github index 9924450f..93b681eb 100644 --- a/config/config.yml_example_github +++ b/config/config.yml_example_github @@ -21,11 +21,4 @@ oauth: provider: github client_id: xxxxxxxxxxxxxxxxxxxx client_secret: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx - - # these GitHub OAuth defaults are set for you.. - # from https://godoc.org/golang.org/x/oauth2/github - # auth_url: https://github.com/login/oauth/authorize - # token_url: https://github.com/login/oauth/access_token - # scopes: - # - user - # user_info_url: https://api.github.com/user?access_token= \ No newline at end of file + # endpoints set from https://godoc.org/golang.org/x/oauth2/github From c5f2ab165a431e2559e0e830e4498b2f49b0831b Mon Sep 17 00:00:00 2001 From: Benjamin Foote Date: Fri, 19 Oct 2018 11:13:58 -0700 Subject: [PATCH 057/360] minor edit as per #22 --- config/config.yml_example_github_enterprise | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/config/config.yml_example_github_enterprise b/config/config.yml_example_github_enterprise index 3d1eec6e..79e73b8f 100644 --- a/config/config.yml_example_github_enterprise +++ b/config/config.yml_example_github_enterprise @@ -24,8 +24,7 @@ oauth: client_secret: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx auth_url: https://githubenterprise.yoursite.com/login/oauth/authorize token_url: https://githubenterprise.yoursite.com/login/oauth/access_token - user_info_url: https://githubenterprise.yoursite.com/user?access_token= - + user_info_url: https://githubenterprise.yoursite.com/api/v3/user?access_token= # these GitHub OAuth defaults are set for you.. # scopes: # - user From 0a048eb7f14d0f4ecbc67ca1a082e66c42c0e85c Mon Sep 17 00:00:00 2001 From: Benjamin Foote Date: Fri, 19 Oct 2018 13:10:23 -0700 Subject: [PATCH 058/360] general cleanup of README, do.sh, TODO --- README.md | 19 +++++---- TODO.md | 87 +++++++++++++++------------------------ do.sh | 34 ++++----------- lasso_flow.dot | 41 ------------------ lasso_flow.png | Bin 176681 -> 0 bytes pkg/response/response.go | 6 +-- 6 files changed, 54 insertions(+), 133 deletions(-) delete mode 100644 lasso_flow.dot delete mode 100644 lasso_flow.png diff --git a/README.md b/README.md index 0f8aefaa..03f76f80 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ an SSO solution for nginx using the [auth_request](http://nginx.org/en/docs/http/ngx_http_auth_request_module.html) module. -lasso supports OAuth login via Google, [GitHub](https://developer.github.com/apps/building-integrations/setting-up-and-registering-oauth-apps/about-authorization-options-for-oauth-apps/), [IndieAuth](https://indieauth.spec.indieweb.org/), and OpenID Connect providers. +lasso supports OAuth login via Google, [GitHub](https://developer.github.com/apps/building-integrations/setting-up-and-registering-oauth-apps/about-authorization-options-for-oauth-apps/), [IndieAuth](https://indieauth.spec.indieweb.org/), and OpenID Connect providers If lasso is running on the same host as the nginx reverse proxy the response time from the `/validate` endpoint to nginx should be less than 1ms @@ -11,7 +11,7 @@ For support please file tickets here or visit our IRC channel [#lasso](irc://fre ## Installation * `cp ./config/config.yml_example ./config/config.yml` -* create oauth credentials for lasso at [google](https://console.developers.google.com/apis/credentials) or [github](https://developer.github.com/apps/building-integrations/setting-up-and-registering-oauth-apps/about-authorization-options-for-oauth-apps/) +* create OAuth credentials for lasso at [google](https://console.developers.google.com/apis/credentials) or [github](https://developer.github.com/apps/building-integrations/setting-up-and-registering-oauth-apps/about-authorization-options-for-oauth-apps/) * be sure to direct the callback URL to the `/auth` endpoint * configure nginx... @@ -78,11 +78,16 @@ server { ## Running from Docker -* `./do.sh drun` - -And that's it! Or if you can examine the docker command in `do.sh` +```bash +docker run -d \ + -p 9090:9090 \ + --name lasso \ + -v ${PWD}/config:/config \ + -v ${PWD}/data:/data \ + lassoproject/lasso +``` -The [bfoote/lasso](https://hub.docker.com/r/bfoote/lasso/) Docker image is an automated build on Docker Hub +The [lassoproject/lasso](https://hub.docker.com/r/lassoproject/lasso/) Docker image is an automated build on Docker Hub ## Running from source @@ -134,4 +139,4 @@ Note that outside of some innocuos redirection, Bob only ever sees `https://priv Once the JWT is set, Bob will be authorized for all other sites which are configured to use `https://lasso.oursites.com/validate` from the `auth_request` nginx module. -The next time Bob is forwarded to google for login, since he has already authorized the site it immediately forwards him back and sets the cookie and sends him on his merry way. Bob may not even notice that he logged in via lasso. +The next time Bob is forwarded to google for login, since he has already authorized the lasso OAuth app, Google immediately forwards him back and sets the cookie and sends him on his merry way. Bob may not even notice that he logged in via lasso. diff --git a/TODO.md b/TODO.md index c04a1471..5dc0409c 100644 --- a/TODO.md +++ b/TODO.md @@ -1,65 +1,17 @@ -## questions for golang meetup - -* how do I populate the context with the return code for later logging? -* where should I put my pkgs? - -## TODO - -* aaronpk 2017-10-04 - ‎[15:46] ‎<‎aaronpk‎>‎ so, immediate feature request is to be able to whitelist specific email addresses instead of doing domain matching for users - - -* aaronpk - ‎[16:40] ‎<‎aaronpk‎>‎ sure! basically i want to redirect to https://indieauth.com instead of google auth - ‎[16:41] ‎<‎aaronpk‎>‎ and there's an endpoint there that the plugin can use to verify the auth code and get user info - ‎[16:44] ‎<‎aaronpk‎>‎ so being able to customize this URL or maybe even override some method to be ableto customize the handling of the verification https://github.com/bnfinet/lasso/blob/master/handlers/handlers.go#L313 - ‎[16:49] ‎<‎aaronpk‎>‎ here's the docs i was walking you through https://indieauth.com/developers - ‎[16:53] ‎<‎bfoote‎>‎ oh that's looks pretty straight forward - -* add config for oauth Enpoint - https://github.com/golang/oauth2/blob/master/github/github.go - if endpoing is ~= google then allow 'hd' and accomodate getting User info - * is user info for Oauth a standard form? Probably _no_. Going to need some interpreters. - -* create a special team for admins - -* look for the token in an "Authorization: bearer $TOKEN" header +# TODO * include static assets in binary https://github.com/shurcooL/vfsgen -* restapi - * `/api/validate` endpoint that *any* service can connect to that validates the `X-LASSO-TOKEN` header - -* add lastupdate to user, sites, team - * handle multiple domains * set the `Oauth2.config{RedirectURL}` Google callback URL dynamically based on the domain that was offered - * iterate through a list of authorized domains * 302 redirect to the next domain * set a jwt cookie into each domain * might slow down login -* how to handle "not authorized for domain"? - * can nginx pass a 302 back to /login with an argument in the querystring such as.. - /login?jwt=$COOKIE - yes it can! using the auth_request_set $variable value; - `auth_request_set $auth_lasso_redirect $upstream_http_lasso_redirect` - http://nginx.org/en/docs/http/ngx_http_auth_request_module.html#auth_request_set - * but we're forgetting about the round trip from the state login and setting the cookie - * we just need to detect if we've been here several times in a row, using state and then provide some kind of auth error - * try three times, then provide auth error - - * issue tokens manually for webhooks - * any of these are valid.. - * http cookie contents - * X-Lasso-Token: ${TOKEN} - * Authorization: Bearer ${TOKEN} - * ?lasso-token=${TOKEN} - * TODO is this the order that these are evaluated in? * tokens are special * set the "issuer" field to the user * does user exist? @@ -76,23 +28,42 @@ * pobably yes * how do we validate the token -* if the user is forwarded to /login a few times, we need to provide some explanation, and offer them an escaltion path or some way forward - * move binaries under a cmd/ subdirectory + * user management * twitter bootstrap * js build environment -* Docker container that's not Dockerfile.fromscratch -* graphviz of Bob visit flow + * additional validations (like what?) ## DONE +* add lastupdate to user, sites, team + +* if the user is forwarded to /login a few times, we need to provide some explanation, and offer them an escaltion path or some way forward + +* `/validate` endpoint that *any* service can connect to that validates the `X-LASSO-TOKEN` header + * any of these are valid.. + * http cookie contents + * X-Lasso-Token: ${TOKEN} + * Authorization: Bearer ${TOKEN} + * ?lasso-token=${TOKEN} + * set X-Lasso-User header passed through to the backend app https://stackoverflow.com/questions/19366215/setting-headers-with-nginx-auth-request-proxy#19366411 * replace gin.Cookie with gorilla.cookie +* how to handle "not authorized for domain"? + * can nginx pass a 302 back to /login with an argument in the querystring such as.. + /login?jwt=$COOKIE + yes it can! using the auth_request_set $variable value; + `auth_request_set $auth_lasso_redirect $upstream_http_lasso_redirect` + http://nginx.org/en/docs/http/ngx_http_auth_request_module.html#auth_request_set + * but we're forgetting about the round trip from the state login and setting the cookie + * we just need to detect if we've been here several times in a row, using state and then provide some kind of auth error + * try three times, then provide auth error + * optionally compress the cookie (gzip && base64) * use url.QueryEscape() instead of base64 https://golang.org/pkg/net/url/#QueryEscape, or maybe use QueryEscape after base64 * can we stuff all the user/sites into a 4093 byte cookie, or perhaps a cookie half that size to leave room for other cookies @@ -133,6 +104,11 @@ yes.. * issue jwt into a cookie for each domain using an image +* add config for oauth Enpoint + https://github.com/golang/oauth2/blob/master/github/github.go + if endpoing is ~= google then allow 'hd' and accomodate getting User info + * is user info for Oauth a standard form? Probably _no_. Going to need some interpreters. + ## leaving teams out of this for now /admin/domains domain rights @@ -149,7 +125,10 @@ * User -## TODO +## TODO web interface + +* restapi +* create a special team for admins * websocket api * `getusers` diff --git a/do.sh b/do.sh index 1c552444..227f49cb 100755 --- a/do.sh +++ b/do.sh @@ -26,15 +26,14 @@ gogo () { docker run --rm -i -t -v /var/run/docker.sock:/var/run/docker.sock -v ${SDIR}/go:/go --name gogo $GOIMAGE $* } -revproxy () { - /home/bfoote/files/docker/bnfinet/dockerfiles/bnfnet/lasso-nginx-test/run_docker.sh $* -} - dbuild () { docker build -f Dockerfile -t $IMAGE . } gobuildstatic () { + # TODO: this doesn't include the templates + # https://github.com/shurcooL/vfsgen + CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main . } @@ -84,6 +83,7 @@ goget () { # install all the things go get -v ./... } + test () { # test all the things if [ -n "$*" ]; then @@ -93,28 +93,9 @@ test () { fi } -graphviz () { -# FILE=$1; shift; - FILE=lasso_flow.dot; - CMD="docker run - --rm - -it - -v $(pwd):/code - -w /code - --entrypoint dot - themarquee/graphviz - -T png - -o lasso_flow.png - $FILE -" - echo $CMD - ${CMD} - -} - DB=data/lasso_bolt.db browsebolt() { - ~/go/bin/boltbrowser $DB + ${GOPATH}/bin/boltbrowser $DB } usage() { @@ -122,14 +103,13 @@ usage() { usage: $0 run - go run main.go $0 build - go build + $0 goget - get all dependencies $0 dbuild - build docker container $0 drun [args] - run docker container $0 test [./pkg_test.go] - run go tests (defaults to all tests) - $0 revproxy - run an nginx reverseproxy for naga.bnf.net $0 browsebolt - browse the boltdb at ${DB} $0 gogo [gocmd] - run, build, any go cmd $0 watch [cmd]] - watch the $CWD for any change and re-reun the [cmd] - $0 graphviz - lasso_flow.dot --> lasso_flow.jpg do is like make @@ -140,7 +120,7 @@ EOF ARG=$1; shift; case "$ARG" in - 'run'|'build'|'browsebolt'|'dbuild'|'drun'|'revproxy'|'graphviz'|'test'|'goget'|'gogo'|'watch'|'gobuildstatic') + 'run'|'build'|'browsebolt'|'dbuild'|'drun'|'test'|'goget'|'gogo'|'watch'|'gobuildstatic') $ARG $* ;; 'godoc') diff --git a/lasso_flow.dot b/lasso_flow.dot deleted file mode 100644 index 50984fa4..00000000 --- a/lasso_flow.dot +++ /dev/null @@ -1,41 +0,0 @@ -# graphviz diagram - -digraph Lasso { - - compound=true; - ratio=fill; node[fontsize=24]; - splines=line; - - browse_to_private_site -> nginx_receive_request; - nginx_receive_request -> validate; - validate -> evaluate_jwt; - evaluate_jwt -> NOT_AUTH; - NOT_AUTH -> redirect_to_login; - redirect_to_login -> redirected_to_login; - redirected_to_login -> login; - login -> redirect_to_google_oauth; - redirect_to_google_oauth -> redirected_to_google_oauth - redirected_to_google_oauth -> google_oauth; - google_oauth -> redirect_to_authorize; - redirect_to_authorize -> authorize; - authorize -> confirm_state; - confirm_state -> state_confirmed; - state_confirmed -> redirect_to_original_url; - redirect_to_original_url -> browse_to_private_site; - - evaluate_jwt -> SUCCESS; - SUCCESS -> set_cookie; - set_cookie -> homepage; - - subgraph cluster_bob { label="Bob"; browse_to_private_site; set_cookie; redirected_to_login; redirected_to_google_oauth} - subgraph cluster_nginx { label="nginx"; nginx_receive_request; NOT_AUTH; SUCCESS; redirect_to_login;} - subgraph cluster_lasso { label="lasso - login.oursites.com"; validate; evaluate_jwt; login; redirect_to_google_oauth; authorize; state_confirmed;} - subgraph cluster_google { label="Google Login"; google_oauth; redirect_to_authorize; confirm_state;} - subgraph cluster_oursite { label="private.oursites.com"; homepage } - - { rank = same; browse_to_private_site; nginx_receive_request; validate; } - { rank = same; evaluate_jwt; NOT_AUTH; redirect_to_login; redirected_to_login;} - { rank = same; login; redirect_to_google_oauth; redirected_to_google_oauth; google_oauth;} - { rank = same; SUCCESS; homepage;} - -} \ No newline at end of file diff --git a/lasso_flow.png b/lasso_flow.png deleted file mode 100644 index fa3ee99dc6d872e49145b75cb60c55cbed534420..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 176681 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 diff --git a/pkg/response/response.go b/pkg/response/response.go index ff7bfd8c..de744959 100644 --- a/pkg/response/response.go +++ b/pkg/response/response.go @@ -16,22 +16,20 @@ type CaptureWriter struct { func (w *CaptureWriter) Write(b []byte) (int, error) { if w.StatusCode == 0 { w.StatusCode = 200 - log.Debugf("set w.StatusCode %d", w.StatusCode) + log.Debugf("CaptureWriter.Write set w.StatusCode %d", w.StatusCode) } - log.Debugf("CaptureWriter.Write code %d", w.StatusCode) return w.ResponseWriter.Write(b) } // Header calls http.Writer.Header() func (w *CaptureWriter) Header() http.Header { - log.Debugf("CaptureWriter.Header code %d", w.StatusCode) return w.ResponseWriter.Header() } // WriteHeader calls http.Writer.WriteHeader(code) func (w *CaptureWriter) WriteHeader(code int) { w.StatusCode = code - log.Debugf("CaptureWriter.WriteHeader code %d", w.StatusCode) + log.Debugf("CaptureWriter.WriteHeader set w.StatusCode %d", w.StatusCode) w.ResponseWriter.WriteHeader(code) } From 795d0b942004a4fb40fcb0c0835115d32294ee60 Mon Sep 17 00:00:00 2001 From: Benjamin Foote Date: Fri, 19 Oct 2018 13:11:41 -0700 Subject: [PATCH 059/360] return only "200 OK" if authorized, unless we are testing --- handlers/handlers.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/handlers/handlers.go b/handlers/handlers.go index 9fec9578..1dd8145e 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -186,13 +186,16 @@ func ValidateRequestHandler(w http.ResponseWriter, r *http.Request) { } } - // renderIndex(w, "user found from email "+user.Email) w.Header().Add(cfg.Cfg.Headers.User, claims.Username) w.Header().Add(cfg.Cfg.Headers.Success, "true") log.WithFields(log.Fields{cfg.Cfg.Headers.User: w.Header().Get(cfg.Cfg.Headers.User)}).Debug("response header") // good to go!! - ok200(w, r) + if cfg.Cfg.Testing { + renderIndex(w, "user authorized "+claims.Username) + } else { + ok200(w, r) + } // TODO // parse the jwt and see if the claim is valid for the domain From d96a56385426fe088ca2d76ed575421d503f93b0 Mon Sep 17 00:00:00 2001 From: Benjamin Foote Date: Fri, 19 Oct 2018 13:27:25 -0700 Subject: [PATCH 060/360] write 200 OK in response body --- handlers/handlers.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/handlers/handlers.go b/handlers/handlers.go index 1dd8145e..b3502442 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -579,10 +579,8 @@ func redirect302(w http.ResponseWriter, r *http.Request, rURL string) { } func ok200(w http.ResponseWriter, r *http.Request) { - - n, err := w.Write(nil) + _, err := w.Write([]byte("200 OK\n")) if err != nil { log.Error(err) } - log.Debugf("ok200 with empty body (bytes %d)", n) } From b4a9f921f2a9fab831d770931e83ee597df66a14 Mon Sep 17 00:00:00 2001 From: Benjamin Foote Date: Fri, 19 Oct 2018 15:42:55 -0700 Subject: [PATCH 061/360] modify example for ssl/443 and move error401 --- README.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 03f76f80..e51794d1 100644 --- a/README.md +++ b/README.md @@ -17,14 +17,15 @@ For support please file tickets here or visit our IRC channel [#lasso](irc://fre ```{.nginxconf} server { - listen 80 default_server; + listen 443 ssl http2; server_name dev.yourdomain.com; root /var/www/html/; + ssl_certificate /etc/letsencrypt/live/dev.yourdomain.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/dev.yourdomain.com/privkey.pem; + # send all requests to the `/validate` endpoint for authorization auth_request /validate; - # if validate returns `401 not authorized` then forward the request to the error401block - error_page 401 = @error401; location = /validate { # lasso can run behind the same nginx-revproxy @@ -44,6 +45,9 @@ server { auth_request_set $auth_resp_failcount $upstream_http_x_lasso_failcount; } + # if validate returns `401 not authorized` then forward the request to the error401block + error_page 401 = @error401; + location @error401 { # redirect to lasso for login return 302 https://lasso.yourdomain.com:9090/login?url=$scheme://$http_host$request_uri&lasso-failcount=$auth_resp_failcount&X-Lasso-Token=$auth_resp_jwt&error=$auth_resp_err; From 683db55fba7c183c3fc42e7f8f2ab47856b0e63c Mon Sep 17 00:00:00 2001 From: Benjamin Foote Date: Sat, 3 Nov 2018 11:51:56 -0700 Subject: [PATCH 062/360] add version and semver to binary and display at startup --- Dockerfile | 7 +++++-- do.sh | 4 ++-- main.go | 10 +++++++++- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9a74f289..eac1aa29 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,13 +2,16 @@ # https://github.com/LassoProject/lasso FROM golang:1.8 +LABEL maintainer="lasso@bnf.net" + RUN mkdir -p ${GOPATH}/src/github.com/LassoProject/lasso WORKDIR ${GOPATH}/src/github.com/LassoProject/lasso COPY . . -RUN go-wrapper download # "go get -d -v ./..." -RUN go-wrapper install # "go install -v ./..." +RUN go-wrapper download # "go get -d -v ./..." \ + && go-wrapper build # "go install -v ./..." \ + && ./do.sh build # see `do.sh` for lasso build details RUN rm -rf ./config ./data \ && ln -s /config ./config \ diff --git a/do.sh b/do.sh index 227f49cb..3c542890 100755 --- a/do.sh +++ b/do.sh @@ -8,7 +8,7 @@ cd $SDIR export LASSO_ROOT=${GOPATH}/src/github.com/LassoProject/lasso/ -IMAGE=bfoote/lasso +IMAGE=lassoproject/lasso GOIMAGE=golang:1.8 NAME=lasso HTTPPORT=9090 @@ -19,7 +19,7 @@ run () { } build () { - go build . + go build -i -v -ldflags="-X main.version=$(git describe --always --long) -X main.semver=v$(git semver get)" . } gogo () { diff --git a/main.go b/main.go index 64ce5d3a..9128ff55 100644 --- a/main.go +++ b/main.go @@ -16,6 +16,11 @@ import ( tran "github.com/LassoProject/lasso/pkg/transciever" ) +// version ang semver get overwritten by build with +// go build -i -v -ldflags="-X main.version=$(git describe --always --long) -X main.semver=v$(git semver get)" +var version = "undefined" +var semver = "undefined" + func main() { log.Info("starting lasso") mux := http.NewServeMux() @@ -46,7 +51,10 @@ func main() { // http.Handle("/socket.io/", tran.Server) var listen = cfg.Cfg.Listen + ":" + strconv.Itoa(cfg.Cfg.Port) - log.Infof("running lasso on %s", listen) + log.WithFields(log.Fields{ + "semver": semver, + "version": version, + "listen": listen}).Info("running lasso") srv := &http.Server{ Handler: mux, From 47859086fe9191079c0c13bb11b0406d791d4793 Mon Sep 17 00:00:00 2001 From: Benjamin Foote Date: Sat, 3 Nov 2018 16:56:05 -0700 Subject: [PATCH 063/360] test for tcp port availability at startup, set branding --- handlers/handlers.go | 2 +- pkg/cfg/cfg.go | 105 +++++++++++++++++++++++++++---------------- pkg/cfg/cfg_test.go | 2 +- 3 files changed, 69 insertions(+), 40 deletions(-) diff --git a/handlers/handlers.go b/handlers/handlers.go index b3502442..db0e1fb6 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -330,7 +330,7 @@ func VerifyUser(u interface{}) (ok bool, err error) { } } } else if len(cfg.Cfg.Domains) != 0 && !domains.IsUnderManagement(user.Email) { - err = fmt.Errorf("Email %s is not within a lasso managed domain", user.Email) + err = fmt.Errorf("Email %s is not within a "+cfg.Branding.+" managed domain", user.Email) // } else if !domains.IsUnderManagement(user.HostDomain) { // err = fmt.Errorf("HostDomain %s is not within a lasso managed domain", u.HostDomain) } else { diff --git a/pkg/cfg/cfg.go b/pkg/cfg/cfg.go index e7cc544a..5597e1b5 100644 --- a/pkg/cfg/cfg.go +++ b/pkg/cfg/cfg.go @@ -5,7 +5,9 @@ import ( "flag" "io/ioutil" "math/rand" + "net" "os" + "strconv" "time" log "github.com/Sirupsen/logrus" @@ -78,7 +80,16 @@ type OAuthProviders struct { OIDC string } +type branding struct { + LCName string // lower case + UCName string // upper case + CcName string // camel case +} + var ( + // Branding that's our name + Branding = branding{"lasso", "LASSO", "Lasso"} + // Cfg the main exported config variable Cfg config @@ -118,6 +129,18 @@ func init() { } setDefaults() + + errT := BasicTest() + if errT != nil { + // log.Fatalf(errT.Error()) + panic(errT) + } + + var listen = Cfg.Listen + ":" + strconv.Itoa(Cfg.Port) + if !isTCPPortAvailable(listen) { + log.Fatal(errors.New("check the port availability (is " + Branding.CcName + " already running?)")) + } + log.Debug(viper.AllSettings()) } @@ -126,18 +149,13 @@ func ParseConfig() { log.Debug("opening config") viper.SetConfigName("config") viper.SetConfigType("yaml") - viper.AddConfigPath(os.Getenv("LASSO_ROOT") + "config") + viper.AddConfigPath(os.Getenv(Branding.UCName+"_ROOT") + "config") err := viper.ReadInConfig() // Find and read the config file if err != nil { // Handle errors reading the config file log.Fatalf("Fatal error config file: %s", err.Error()) panic(err) } - UnmarshalKey("lasso", &Cfg) - errT := BasicTest() - if errT != nil { - // log.Fatalf(err.prob) - panic(errT) - } + UnmarshalKey(Branding.LCName, &Cfg) // don't log the secret! // log.Debugf("secret: %s", string(Cfg.JWT.Secret)) } @@ -170,89 +188,89 @@ func setDefaults() { // https://github.com/spf13/viper/issues/309 // viper.SetDefault("listen", "0.0.0.0") // viper.SetDefault(Cfg.Port, 9090) - // viper.SetDefault("Headers.SSO", "X-Lasso-Token") - // viper.SetDefault("Headers.Redirect", "X-Lasso-Requested-URI") + // viper.SetDefault("Headers.SSO", "X-"+Branding.CcName+"-Token") + // viper.SetDefault("Headers.Redirect", "X-"+Branding.CcName+"-Requested-URI") // viper.SetDefault("Cookie.Name", "Lasso") // logging - if !viper.IsSet("lasso.logLevel") { + if !viper.IsSet(Branding.LCName + ".logLevel") { Cfg.LogLevel = "info" } // network defaults - if !viper.IsSet("lasso.listen") { + if !viper.IsSet(Branding.LCName + ".listen") { Cfg.Listen = "0.0.0.0" } - if !viper.IsSet("lasso.port") { + if !viper.IsSet(Branding.LCName + ".port") { Cfg.Port = 9090 } - if !viper.IsSet("lasso.allowAllUsers") { + if !viper.IsSet(Branding.LCName + ".allowAllUsers") { Cfg.AllowAllUsers = false } - if !viper.IsSet("lasso.publicAccess") { + if !viper.IsSet(Branding.LCName + ".publicAccess") { Cfg.PublicAccess = false } // jwt defaults - if !viper.IsSet("lasso.jwt.secret") { + if !viper.IsSet(Branding.LCName + ".jwt.secret") { Cfg.JWT.Secret = getOrGenerateJWTSecret() } - if !viper.IsSet("lasso.jwt.issuer") { - Cfg.JWT.Issuer = "Lasso" + if !viper.IsSet(Branding.LCName + ".jwt.issuer") { + Cfg.JWT.Issuer = Branding.CcName } - if !viper.IsSet("lasso.jwt.maxAge") { + if !viper.IsSet(Branding.LCName + ".jwt.maxAge") { Cfg.JWT.MaxAge = 240 } - if !viper.IsSet("lasso.jwt.compress") { + if !viper.IsSet(Branding.LCName + ".jwt.compress") { Cfg.JWT.Compress = true } // cookie defaults - if !viper.IsSet("lasso.cookie.name") { + if !viper.IsSet(Branding.LCName + ".cookie.name") { Cfg.Cookie.Name = "LassoCookie" } - if !viper.IsSet("lasso.cookie.secure") { + if !viper.IsSet(Branding.LCName + ".cookie.secure") { Cfg.Cookie.Secure = false } - if !viper.IsSet("lasso.cookie.httpOnly") { + if !viper.IsSet(Branding.LCName + ".cookie.httpOnly") { Cfg.Cookie.HTTPOnly = true } // headers defaults - if !viper.IsSet("lasso.headers.jwt") { - Cfg.Headers.JWT = "X-Lasso-Token" + if !viper.IsSet(Branding.LCName + ".headers.jwt") { + Cfg.Headers.JWT = "X-" + Branding.CcName + "-Token" } - if !viper.IsSet("lasso.headers.querystring") { + if !viper.IsSet(Branding.LCName + ".headers.querystring") { Cfg.Headers.QueryString = "access_token" } - if !viper.IsSet("lasso.headers.redirect") { - Cfg.Headers.Redirect = "X-Lasso-Requested-URI" + if !viper.IsSet(Branding.LCName + ".headers.redirect") { + Cfg.Headers.Redirect = "X-" + Branding.CcName + "-Requested-URI" } - if !viper.IsSet("lasso.headers.user") { - Cfg.Headers.User = "X-Lasso-User" + if !viper.IsSet(Branding.LCName + ".headers.user") { + Cfg.Headers.User = "X-" + Branding.CcName + "-User" } - if !viper.IsSet("lasso.headers.success") { - Cfg.Headers.Success = "X-Lasso-Success" + if !viper.IsSet(Branding.LCName + ".headers.success") { + Cfg.Headers.Success = "X-" + Branding.CcName + "-Success" } // db defaults - if !viper.IsSet("lasso.db.file") { - Cfg.DB.File = "data/lasso_bolt.db" + if !viper.IsSet(Branding.LCName + ".db.file") { + Cfg.DB.File = "data/" + Branding.LCName + "_bolt.db" } // session HERE - if !viper.IsSet("lasso.session.name") { - Cfg.Session.Name = "lassoSession" + if !viper.IsSet(Branding.LCName + ".session.name") { + Cfg.Session.Name = Branding.LCName + "Session" } // testing convenience variable - if !viper.IsSet("lasso.testing") { + if !viper.IsSet(Branding.LCName + ".testing") { Cfg.Testing = false } - if viper.IsSet("lasso.test_url") { + if viper.IsSet(Branding.LCName + ".test_url") { Cfg.TestURLs = append(Cfg.TestURLs, Cfg.TestURL) } // TODO: proably change this name, maybe set the domain/port the webapp runs on - if !viper.IsSet("lasso.webapp") { + if !viper.IsSet(Branding.LCName + ".webapp") { Cfg.WebApp = false } @@ -351,3 +369,14 @@ func getOrGenerateJWTSecret() string { } return string(b) } + +func isTCPPortAvailable(listen string) bool { + log.Debug("checking availability of tcp port: " + listen) + conn, err := net.Listen("tcp", listen) + if err != nil { + log.Error(err) + return false + } + conn.Close() + return true +} diff --git a/pkg/cfg/cfg_test.go b/pkg/cfg/cfg_test.go index a19726b2..4b4734a0 100644 --- a/pkg/cfg/cfg_test.go +++ b/pkg/cfg/cfg_test.go @@ -18,7 +18,7 @@ func init() { func TestConfigParsing(t *testing.T) { - UnmarshalKey("lasso", &cfg) + UnmarshalKey(Branding.LCName, &cfg) log.Debugf("cfgPort %d", cfg.Port) log.Debugf("cfgDomains %s", cfg.Domains[0]) From 8e9de2a142b90035c16f211215e8701e00294150 Mon Sep 17 00:00:00 2001 From: Benjamin Foote Date: Sat, 3 Nov 2018 16:57:03 -0700 Subject: [PATCH 064/360] use go 1.10, include branch semver build info into binary --- Dockerfile | 12 ++++++++---- do.sh | 16 +++++++++++++--- main.go | 31 ++++++++++++++++++++++--------- 3 files changed, 43 insertions(+), 16 deletions(-) diff --git a/Dockerfile b/Dockerfile index eac1aa29..39474ede 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # lassoproject/lasso # https://github.com/LassoProject/lasso -FROM golang:1.8 +FROM golang:1.10 LABEL maintainer="lasso@bnf.net" @@ -9,9 +9,13 @@ WORKDIR ${GOPATH}/src/github.com/LassoProject/lasso COPY . . -RUN go-wrapper download # "go get -d -v ./..." \ - && go-wrapper build # "go install -v ./..." \ - && ./do.sh build # see `do.sh` for lasso build details +# RUN go-wrapper download # "go get -d -v ./..." +# RUN ./do.sh build # see `do.sh` for lasso build details +# RUN go-wrapper install # "go install -v ./..." + +RUN go get -d -v ./... +RUN ./do.sh build # see `do.sh` for lasso build details +RUN ./do.sh install RUN rm -rf ./config ./data \ && ln -s /config ./config \ diff --git a/do.sh b/do.sh index 3c542890..eac3d498 100755 --- a/do.sh +++ b/do.sh @@ -9,7 +9,7 @@ cd $SDIR export LASSO_ROOT=${GOPATH}/src/github.com/LassoProject/lasso/ IMAGE=lassoproject/lasso -GOIMAGE=golang:1.8 +GOIMAGE=golang:1.10 NAME=lasso HTTPPORT=9090 GODOC_PORT=5050 @@ -19,7 +19,16 @@ run () { } build () { - go build -i -v -ldflags="-X main.version=$(git describe --always --long) -X main.semver=v$(git semver get)" . + local VERSION=$(git describe --always --long) + local DT=$(date --rfc-3339=seconds --universal| sed 's/ /T/') + local FQDN=$(hostname --fqdn) + local SEMVER=$(git tag --list --sort="v:refname" | tail -n -1) + local BRANCH=$(git rev-parse --abbrev-ref HEAD) + go build -i -v -ldflags=" -X main.version=${VERSION} -X main.builddt=${DT} -X main.host=${FQDN} -X main.semver=${SEMVER} -X main.branch=${BRANCH}" . +} + +install () { + cp ./lasso ${GOPATH}/bin/lasso } gogo () { @@ -103,6 +112,7 @@ usage() { usage: $0 run - go run main.go $0 build - go build + $0 install - move binary to ${GOPATH}/bin/lasso $0 goget - get all dependencies $0 dbuild - build docker container $0 drun [args] - run docker container @@ -120,7 +130,7 @@ EOF ARG=$1; shift; case "$ARG" in - 'run'|'build'|'browsebolt'|'dbuild'|'drun'|'test'|'goget'|'gogo'|'watch'|'gobuildstatic') + 'run'|'build'|'browsebolt'|'dbuild'|'drun'|'install'|'test'|'goget'|'gogo'|'watch'|'gobuildstatic') $ARG $* ;; 'godoc') diff --git a/main.go b/main.go index 9128ff55..adc9e69e 100644 --- a/main.go +++ b/main.go @@ -18,11 +18,30 @@ import ( // version ang semver get overwritten by build with // go build -i -v -ldflags="-X main.version=$(git describe --always --long) -X main.semver=v$(git semver get)" -var version = "undefined" -var semver = "undefined" + +var ( + version = "undefined" + builddt = "undefined" + host = "undefined" + semver = "undefined" + branch = "undefined" +) + +func init() { + // var listen = cfg.Cfg.Listen + ":" + strconv.Itoa(cfg.Cfg.Port) +} func main() { - log.Info("starting lasso") + var listen = cfg.Cfg.Listen + ":" + strconv.Itoa(cfg.Cfg.Port) + log.WithFields(log.Fields{ + // "semver": semver, + "version": version, + "buildtime": builddt, + "buildhost": host, + "branch": branch, + "semver": semver, + "listen": listen}).Info("starting " + cfg.Branding) + mux := http.NewServeMux() authH := http.HandlerFunc(handlers.ValidateRequestHandler) @@ -50,12 +69,6 @@ func main() { // mux.Handle("/socket.io/", cors.AllowAll(socketio)) // http.Handle("/socket.io/", tran.Server) - var listen = cfg.Cfg.Listen + ":" + strconv.Itoa(cfg.Cfg.Port) - log.WithFields(log.Fields{ - "semver": semver, - "version": version, - "listen": listen}).Info("running lasso") - srv := &http.Server{ Handler: mux, Addr: listen, From 1358ee30f6c5af1ac8f49f53fc9c2581a8e9fdfd Mon Sep 17 00:00:00 2001 From: Benjamin Foote Date: Sat, 3 Nov 2018 17:04:36 -0700 Subject: [PATCH 065/360] set branding --- handlers/handlers.go | 2 +- main.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/handlers/handlers.go b/handlers/handlers.go index db0e1fb6..185a1f5d 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -330,7 +330,7 @@ func VerifyUser(u interface{}) (ok bool, err error) { } } } else if len(cfg.Cfg.Domains) != 0 && !domains.IsUnderManagement(user.Email) { - err = fmt.Errorf("Email %s is not within a "+cfg.Branding.+" managed domain", user.Email) + err = fmt.Errorf("Email %s is not within a "+cfg.Branding.CcName+" managed domain", user.Email) // } else if !domains.IsUnderManagement(user.HostDomain) { // err = fmt.Errorf("HostDomain %s is not within a lasso managed domain", u.HostDomain) } else { diff --git a/main.go b/main.go index adc9e69e..f0b49bf8 100644 --- a/main.go +++ b/main.go @@ -40,7 +40,7 @@ func main() { "buildhost": host, "branch": branch, "semver": semver, - "listen": listen}).Info("starting " + cfg.Branding) + "listen": listen}).Info("starting " + cfg.Branding.CcName) mux := http.NewServeMux() From 2eb2a6b0ca67b1a38577b8a1aebdc4f91fdb8cc4 Mon Sep 17 00:00:00 2001 From: Benjamin Foote Date: Sat, 3 Nov 2018 17:18:28 -0700 Subject: [PATCH 066/360] tell the end user that they're in testing mode --- handlers/handlers.go | 3 ++- templates/index.tmpl | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/handlers/handlers.go b/handlers/handlers.go index 185a1f5d..d407de95 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -28,6 +28,7 @@ import ( type Index struct { Msg string TestURLs []string + Testing bool } // AuthError sets the values to return to nginx @@ -301,7 +302,7 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) { } func renderIndex(w http.ResponseWriter, msg string) { - if err := indexTemplate.Execute(w, &Index{Msg: msg, TestURLs: cfg.Cfg.TestURLs}); err != nil { + if err := indexTemplate.Execute(w, &Index{Msg: msg, TestURLs: cfg.Cfg.TestURLs, Testing: cfg.Cfg.Testing}); err != nil { log.Error(err) } } diff --git a/templates/index.tmpl b/templates/index.tmpl index 31ba1acc..11fd6147 100644 --- a/templates/index.tmpl +++ b/templates/index.tmpl @@ -6,7 +6,10 @@ Lasso: {{ .Msg }}. - +{{ if .Testing }} +

    -- test mode --

    +config file includes testing: true +{{ end }}

    {{ .Msg }}.