From 04c4cf7240ab22f7293e26c13b1ae56f820bdebb Mon Sep 17 00:00:00 2001 From: Daniel Wood Date: Tue, 5 May 2020 13:23:38 -0700 Subject: [PATCH] Revert "Merge branch 'master' into regexWhiteList" This reverts commit 3fdcd5f4fd2cf6cf68e665c4cff921533f3b6ac8, reversing changes made to b0b7fab64b9d46eee5d1a8dfc1c5ce68b743a4f0. --- LICENSE | 2 +- README.md | 48 +- config/config.yml_example | 14 +- config/testing/handler_claims.yml | 1 - config/testing/handler_login_url.yml | 15 - config/testing/handler_logout_url.yml | 20 - do.sh | 124 ++--- handlers/adfs/adfs.go | 10 - handlers/auth.go | 149 ----- handlers/common/common.go | 10 - handlers/github/github.go | 10 - handlers/github/github_test.go | 10 - handlers/google/google.go | 10 - handlers/handlers.go | 700 ++++++++++++++++++++---- handlers/handlers_test.go | 56 +- handlers/healthcheck.go | 25 - handlers/homeassistant/homeassistant.go | 10 - handlers/indieauth/indieauth.go | 10 - handlers/login.go | 193 ------- handlers/login_test.go | 53 -- handlers/logout.go | 53 -- handlers/logout_test.go | 47 -- handlers/nextcloud/nextcloud.go | 10 - handlers/openid/openid.go | 10 - handlers/openstax/openstax.go | 10 - handlers/responses.go | 62 --- handlers/validate.go | 184 ------- main.go | 38 +- pkg/cfg/cfg.go | 74 +-- pkg/cfg/cfg_test.go | 16 +- pkg/cfg/jwt.go | 10 - pkg/cfg/logging.go | 23 +- pkg/cfg/logging_test.go | 10 - pkg/cfg/oauth.go | 50 +- pkg/cfg/oauth_test.go | 42 -- pkg/cookie/cookie.go | 47 +- pkg/cookie/cookie_test.go | 10 - pkg/domains/domains.go | 10 - pkg/domains/domains_test.go | 10 - pkg/healthcheck/healthcheck.go | 10 - pkg/jwtmanager/jwtmanager.go | 10 - pkg/jwtmanager/jwtmanager_test.go | 10 - pkg/response/response.go | 10 - pkg/structs/structs.go | 10 - pkg/timelog/timelog.go | 10 - 45 files changed, 741 insertions(+), 1505 deletions(-) delete mode 100644 config/testing/handler_login_url.yml delete mode 100644 config/testing/handler_logout_url.yml delete mode 100644 handlers/auth.go delete mode 100644 handlers/healthcheck.go delete mode 100644 handlers/login.go delete mode 100644 handlers/login_test.go delete mode 100644 handlers/logout.go delete mode 100644 handlers/logout_test.go delete mode 100644 handlers/responses.go delete mode 100644 handlers/validate.go delete mode 100644 pkg/cfg/oauth_test.go diff --git a/LICENSE b/LICENSE index cbd1b0fd..313e9ff7 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2017 The Vouch Proxy Authors +Copyright (c) 2017 Benjamin Foote Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 0483beef..893a0794 100644 --- a/README.md +++ b/README.md @@ -180,19 +180,7 @@ Helm Charts are maintained by [halkeye](https://github.com/halkeye) and are avai ./vouch-proxy ``` -## /login and /logout endpoint redirection - -As of `v0.11.0` we have put additional checks in place to reduce [the attack surface of url redirection](https://blog.detectify.com/2019/05/16/the-real-impact-of-an-open-redirect/). - -### /login?url=POST_LOGIN_URL - -The passed URL... - -- must start with either `http` or `https` -- must have a domain overlap with either a domain in the `vouch.domains` list or the `vouch.cookie.domain` (if either of those are configured) -- cannot have a parameter which includes a URL to [prevent URL chaining attacks](https://hackerone.com/reports/202781) - -### /logout?url=NEXT_URL +## /logout endpoint redirection The Vouch Proxy `/logout` endpoint accepts a `url` parameter in the query string which can be used to `302` redirect a user to your orignal OAuth provider/IDP/OIDC provider's [revocation_endpoint](https://tools.ietf.org/html/rfc7009) @@ -200,30 +188,13 @@ The Vouch Proxy `/logout` endpoint accepts a `url` parameter in the query string https://vouch.oursites.com/logout?url=https://oauth2.googleapis.com/revoke ``` -this url must be present in the configuration file on the list `vouch.post_logout_redirect_uris` - -```yaml -# in order to prevent redirection attacks all redirected URLs to /logout must be specified -# the URL must still be passed to Vouch Proxy as https://vouch.yourdomain.com/logout?url=${ONE OF THE URLS BELOW} -post_logout_redirect_uris: - # your apps login page - - http://.yourdomain.com/login - # your IdPs logout enpoint - # from https://accounts.google.com/.well-known/openid-configuration - - https://oauth2.googleapis.com/revoke - # you may be daisy chaining to your IdP - - https://myorg.okta.com/oauth2/123serverid/v1/logout?post_logout_redirect_uri=http://myapp.yourdomain.com/login -``` - -Note that your IdP will likely carry their own, separate `post_logout_redirect_uri` list. - logout resources.. - [Google](https://developers.google.com/identity/protocols/OAuth2WebServer#tokenrevoke) - [Okta](https://developer.okta.com/docs/api/resources/oidc#logout) - [Auth0](https://auth0.com/docs/logout/guides/logout-idps) -## Troubleshooting, Support and Feature Requests (Read this before submitting an issue at GitHub) +## Troubleshooting, Support and Feature Requests Getting the stars to align between Nginx, Vouch Proxy and your IdP can be tricky. We want to help you get up and running as quickly as possible. The most common problem is.. @@ -266,20 +237,11 @@ Please [submit a new issue](https://github.com/vouch/vouch-proxy/issues) in the - then [open a new issue](https://github.com/vouch/vouch-proxy/issues/new) in this repository - or visit our IRC channel [#vouch](irc://freenode.net/#vouch) on freenode -### submitting a Pull Request for a new feature - -I really love Vouch Proxy! I wish it did XXXX... - -Please make a proposal before you spend your time and our time integrating a new feature. - -Code contributions should.. +### I really love Vouch Proxy! I wish it did XXXX -- include unit tests and in some cases end-to-end tests -- be formatted with `go fmt` -- not break existing setups without a clear reason (usually security related) -- and generally be discussed beforehand in a GitHub issue +Thanks for the love, please open an issue describing your feature or idea before submitting a PR. -For larger contributions or code related to a platform that we don't currently support we will ask you to commit to supporting the feature for an agreed upon period. Invariably someone will pop up here with a question and we want to be able to support these requests. +Please know that Vouch Proxy is not sponsored and is developed and supported on a volunteer basis. ## Advanced Authorization Using OpenResty diff --git a/config/config.yml_example b/config/config.yml_example index c90e052a..e6c0a1cf 100644 --- a/config/config.yml_example +++ b/config/config.yml_example @@ -131,21 +131,9 @@ vouch: # application. This is optional. # idtoken: X-Vouch-IdP-IdToken - # test_url - add this URL to the page which vouch displays during testing (a convenience for testing) + # test_url - add this URL to the page which vouch displays test_url: http://yourdomain.com - # in order to prevent redirection attacks all redirected URLs to /logout must be specified - # the URL must still be passed to Vouch Proxy as https://vouch.yourdomain.com/logout?url=${ONE OF THE URLS BELOW} - # in line with the OIDC spec https://openid.net/specs/openid-connect-session-1_0.html#RedirectionAfterLogout - post_logout_redirect_uris: - # your apps login page - - http://myapp.yourdomain.com/login - # your IdPs logout enpoint - # from https://accounts.google.com/.well-known/openid-configuration - - https://oauth2.googleapis.com/revoke - # you may be daisy chaining to your IdP - - https://myorg.okta.com/oauth2/123serverid/v1/logout?post_logout_redirect_uri=http://myapp.yourdomain.com/login - # # OAuth Provider # configure ONLY ONE of the following oauth providers diff --git a/config/testing/handler_claims.yml b/config/testing/handler_claims.yml index a426f732..8b7e3b8c 100644 --- a/config/testing/handler_claims.yml +++ b/config/testing/handler_claims.yml @@ -11,7 +11,6 @@ vouch: - groups - boolean_claim - family_name - - http://www.example.com/favorite_color cookie: name: vouchTestingCookie diff --git a/config/testing/handler_login_url.yml b/config/testing/handler_login_url.yml deleted file mode 100644 index ff7fdb9a..00000000 --- a/config/testing/handler_login_url.yml +++ /dev/null @@ -1,15 +0,0 @@ -vouch: - domains: - - example.com - - cookie: - secure: false - - jwt: - secret: testingsecret - -oauth: - provider: google - client_id: http://vouch.github.io - auth_url: https://indielogin.com/auth - callback_url: http://vouch.github.io:9090/auth diff --git a/config/testing/handler_logout_url.yml b/config/testing/handler_logout_url.yml deleted file mode 100644 index cc6bd8db..00000000 --- a/config/testing/handler_logout_url.yml +++ /dev/null @@ -1,20 +0,0 @@ -vouch: - domains: - - example.com - - cookie: - secure: false - - jwt: - secret: testingsecret - - post_logout_redirect_uris: - - http://myapp.example.com/login - # https://accounts.google.com/.well-known/openid-configuration - - https://oauth2.googleapis.com/revoke - -oauth: - provider: google - client_id: http://vouch.github.io - auth_url: https://indielogin.com/auth - callback_url: http://vouch.github.io:9090/auth diff --git a/do.sh b/do.sh index 3aef3c0b..4b6b8e7b 100755 --- a/do.sh +++ b/do.sh @@ -156,36 +156,45 @@ test() { } test_logging() { - build + # use Process Substitution to capture log output + # https://stackoverflow.com/questions/20017805/bash-capture-output-of-command-run-in-background + # set -b + # set -o notify + build declare -a levels=(error warn info debug) echo "testing loglevel set from command line" levelcount=0 for ll in ${levels[*]}; do # test that we can see the current level and no level below this level + coproc vpll (./vouch-proxy -logtest -loglevel ${ll} -config ./config/testing/test_config.yml) + exec 2> /dev/null # suppress process terminated messages since ubuntu's kill doesn't support `kill -0` + + # echo "log level $ll vouch-proxy pid ${vpll_PID} fd ${vpll[0]}"; + llpid=${vpll_PID} + # declare -a shouldnotfind=(info) declare -a shouldnotfind=() for (( i=0; i<${#levels[@]}; i++ )); do - if (( $i > $levelcount )); then + if (( i > $levelcount )); then shouldnotfind+=(${levels[$i]}) fi done linesread=0 - IFS=$'\n';for line in $(./vouch-proxy -logtest -loglevel ${ll} -config ./config/testing/test_config.yml); do + while read -t 1 -u ${vpll[0]} line; do let "linesread+=1" # echo "$linesread $line" - # first line is log info - if (( $linesread > 1 )); then - for nono in ${shouldnotfind[*]} ; do - if echo $line | grep $nono; then - echo "error: line should not contain '$nono' when loglevel is '$ll'" - echo "$linesread $line" - exit 1; - fi - done - fi + for nono in ${shouldnotfind[*]} ; do + # first line is log info + if (( $linesread > 1 )) && echo $line | grep $nono; then + echo "line should not contain $nono" + echo "$linesread $line" + echo "bad case of the nonos: $nono" + exit 1; + fi + done done let "levelcount+=1" done @@ -195,6 +204,10 @@ test_logging() { levelcount=0 for ll in ${levels[*]}; do # test that we can see the current level and no level below this level + coproc vpll (./vouch-proxy -logtest -config ./config/testing/logging_${ll}.yml ) + # echo "log level $ll vouch-proxy pid ${vpll_PID} fd ${vpll[0]}"; + + # declare -a shouldnotfind=(info) declare -a shouldnotfind=() for (( i=0; i<${#levels[@]}; i++ )); do if (( $i > $levelcount )); then @@ -202,26 +215,26 @@ test_logging() { fi done + # exec 2> /dev/null # suppress process terminated messages linesread=0 - IFS=$'\n';for line in $(./vouch-proxy -logtest -config ./config/testing/logging_${ll}.yml); do + while read -t 1 -u ${vpll[0]} line; do let "linesread+=1" - # the first four messages are log and info when starting from the command line - if (( $linesread > 4 )); then - # echo "$linesread $line" - for nono in ${shouldnotfind[*]} ; do - # echo "testing $nono" - if echo $line | grep $nono; then - echo "error: line should not contain '$nono' when loglevel is '$ll'" - echo "$linesread $line" - exit 1; - fi - done - fi + # echo "$linesread $line" + for nono in ${shouldnotfind[*]} ; do + # the first three messages are log and info when starting from the command line + if (( $linesread > 3 )) && echo $line | grep $nono; then + echo "line should not contain $nono" + echo "$linesread $line" + echo "bad case of the nonos: $nono" + exit 1; + fi + done done let "levelcount+=1" done echo "passed" + killall vouch-proxy exit 0 } @@ -231,60 +244,6 @@ stats () { echo -n "number of go files: " find . -name '*.go' | wc -l - - echo -n "number of covered packages: " - covered=$(coverage | grep ok | wc -l) - echo $covered - echo -n "number of packages not covered: " - coverage | grep -v ok | wc -l - - echo -n "average of coverage for all covered packages: " - sumcoverage=$(coverage | grep ok | awk '{print $5}' | sed 's/%//' | paste -sd+ - | bc) - # echo " sumcoverage: $sumcoverage " - perl -le "print $sumcoverage/$covered, '%'" - exit 0; -} - -license() { - local FILE=$1; - if [ ! -f "${FILE}" ]; then - echo "need filename"; - exit 1; - fi - FOUND=$(_has_license $FILE) - if [ -z "${FOUND}" ]; then - local YEAR=$(git log -1 --format="%ai" -- $FILE | cut -d- -f1); - _print_license $YEAR > ${FILE}_licensed - cat $FILE >> ${FILE}_licensed - mv ${FILE}_licensed $FILE - echo "added license to the header of $FILE" - fi -} - -_print_license() { - local YEAR=$1; - if [ -z "$YEAR" ]; then - YEAR=$(date +%Y) - fi - cat < 0 { + log.Debug("Found claims in config, finding specific keys...") + // Run through all the claims found + for k, v := range claims.CustomClaims { + // Run through the claims we are looking for + for claim, header := range cfg.Cfg.Headers.ClaimsCleaned { + // Check for matching claim + if claim == k { + log.Debug("Found matching claim key: ", k) + // <<<<<<< HEAD + // customHeader := strings.Join([]string{cfg.Cfg.Headers.ClaimHeader, k}, "") + if val, ok := v.([]interface{}); ok { + // ======= + // // convert to string + // val := fmt.Sprint(v) + // if reflect.TypeOf(val).Kind() == reflect.String { + // // if val, ok := v.(string); ok { + // log.Debugf("Adding header for claim %s - %s: %s", k, header, val) + // w.Header().Add(header, val) + // } else if val, ok := v.([]interface{}); ok { + // >>>>>>> master + strs := make([]string, len(val)) + for i, v := range val { + strs[i] = fmt.Sprintf("\"%s\"", v) + } + log.Debugf("Adding header for claim %s - %s: %s", k, header, val) + w.Header().Add(header, strings.Join(strs, ",")) + } else { + // convert to string + val := fmt.Sprint(v) + if reflect.TypeOf(val).Kind() == reflect.String { + // if val, ok := v.(string); ok { + w.Header().Add(header, val) + log.Debugf("Adding header for claim %s - %s: %s", k, header, val) + } else { + log.Errorf("Couldn't parse header type for %s %+v. Please submit an issue.", k, v) + } + } + } + } + } + } + + w.Header().Add(cfg.Cfg.Headers.User, claims.Username) + w.Header().Add(cfg.Cfg.Headers.Success, "true") + + if cfg.Cfg.Headers.AccessToken != "" { + if claims.PAccessToken != "" { + w.Header().Add(cfg.Cfg.Headers.AccessToken, claims.PAccessToken) + } + } + if cfg.Cfg.Headers.IDToken != "" { + if claims.PIdToken != "" { + w.Header().Add(cfg.Cfg.Headers.IDToken, claims.PIdToken) + + } + } + // fastlog.Debugf("response headers %+v", w.Header()) + // fastlog.Debug("response header", + // zap.String(cfg.Cfg.Headers.User, w.Header().Get(cfg.Cfg.Headers.User))) + fastlog.Debug("response header", + zap.Any("all headers", w.Header())) + + // good to go!! + 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 + +} + +// LogoutHandler /logout +// currently performs a 302 redirect to Google +func LogoutHandler(w http.ResponseWriter, r *http.Request) { + log.Debug("/logout") + cookie.ClearCookie(w, r) + + log.Debug("deleting session") + sessstore.MaxAge(-1) + session, err := sessstore.Get(r, cfg.Cfg.Session.Name) + if err != nil { + log.Error(err) + } + if err = session.Save(r, w); err != nil { + log.Error(err) + } + sessstore.MaxAge(300) + + var requestedURL = r.URL.Query().Get("url") + if requestedURL != "" { + redirect302(w, r, requestedURL) + } else { + renderIndex(w, "/logout you have been logged out") + } +} + +// HealthcheckHandler /healthcheck +// just returns 200 '{ "ok": true }' +func HealthcheckHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + if _, err := fmt.Fprintf(w, "{ \"ok\": true }"); err != nil { + log.Error(err) + } +} + +var regExJustAlphaNum, _ = regexp.Compile("[^a-zA-Z0-9]+") + +func generateStateNonce() (string, error) { + state, err := securerandom.URLBase64InBytes(base64Bytes) + if err != nil { + return "", err + } + state = regExJustAlphaNum.ReplaceAllString(state, "") + return state, nil +} + +// LoginHandler /login +// currently performs a 302 redirect to Google +func LoginHandler(w http.ResponseWriter, r *http.Request) { + log.Debug("/login") + // no matter how you ended up here, make sure the cookie gets cleared out + cookie.ClearCookie(w, r) + + session, err := sessstore.Get(r, cfg.Cfg.Session.Name) + if err != nil { + log.Warnf("couldn't find existing encrypted secure cookie with name %s: %s (probably fine)", cfg.Cfg.Session.Name, err) + } + + state, err := generateStateNonce() + if err != nil { + log.Error(err) + } + + // set the state variable in the session + session.Values["state"] = state + log.Debugf("session state set to %s", session.Values["state"]) + + // increment the failure counter for this domain + + // requestedURL comes from nginx in the query string via a 302 redirect + // it sets the ultimate destination + // https://vouch.yoursite.com/login?url= + var requestedURL = r.URL.Query().Get("url") + if requestedURL == "" { + renderIndex(w, "/login no destination URL requested") + log.Error("no destination URL requested") + return + } + + // set session variable for eventual 302 redirecton to original 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 { + failcount = session.Values[requestedURL].(int) + log.Debugf("failcount for %s is %d", requestedURL, failcount) + } + failcount++ + session.Values[requestedURL] = failcount + + log.Debug("saving session") + if err = session.Save(r, w); err != nil { + log.Error(err) + } + + if failcount > 2 { + var vouchError = r.URL.Query().Get("error") + renderIndex(w, "/login too many redirects for "+requestedURL+" - "+vouchError) + } else { + // bounce to oauth provider for login + var lURL = loginURL(r, state) + log.Debugf("redirecting to oauthURL %s", lURL) + redirect302(w, r, lURL) + } +} + +func renderIndex(w http.ResponseWriter, msg string) { + if err := indexTemplate.Execute(w, &Index{Msg: msg, TestURLs: cfg.Cfg.TestURLs, Testing: cfg.Cfg.Testing}); err != nil { + log.Error(err) + } +} + +// VerifyUser validates that the domains match for the user +func VerifyUser(u interface{}) (bool, error) { + + user := u.(structs.User) + + switch { + + // AllowAllUsers + case cfg.Cfg.AllowAllUsers: + log.Debugf("VerifyUser: Success! skipping verification, cfg.Cfg.AllowAllUsers is %t", cfg.Cfg.AllowAllUsers) + return true, nil + + // WhiteList + case len(cfg.Cfg.WhiteList) != 0: + for _, wl := range cfg.Cfg.WhiteList { + if user.Username == wl { + log.Debugf("VerifyUser: Success! found user.Username in WhiteList: %s", user.Username) + return true, nil + } + } + return false, fmt.Errorf("VerifyUser: user.Username not found in WhiteList: %s", user.Username) + + // regexWhiteList + case len(cfg.CompiledRegex) != 0: + for _, wl := range cfg.CompiledRegex { + log.Debugf("Checking claim: '%v' against regex: '%v'", user.Username, wl) + if wl.MatchString(user.Username) { + log.Debugf("VerifyUser: Success! found user.Username in regexWhiteList: %s", user.Username) + return true, nil + } + } + return false, fmt.Errorf("VerifyUser: user.Username not found in regexWhiteList: %s", user.Username) + + // TeamWhiteList + case len(cfg.Cfg.TeamWhiteList) != 0: + for _, team := range user.TeamMemberships { + for _, wl := range cfg.Cfg.TeamWhiteList { + if team == wl { + log.Debugf("VerifyUser: Success! found user.TeamWhiteList in TeamWhiteList: %s for user %s", wl, user.Username) + return true, nil + } + } + } + return false, fmt.Errorf("VerifyUser: user.TeamMemberships %s not found in TeamWhiteList: %s for user %s", user.TeamMemberships, cfg.Cfg.TeamWhiteList, user.Username) + + // Domains + case len(cfg.Cfg.Domains) != 0: + if domains.IsUnderManagement(user.Email) { + log.Debugf("VerifyUser: Success! Email %s found within a "+cfg.Branding.CcName+" managed domain", user.Email) + return true, nil + } + return false, fmt.Errorf("VerifyUser: Email %s is not within a "+cfg.Branding.CcName+" managed domain", user.Email) + + // nothing configured, allow everyone through + default: + log.Warn("VerifyUser: no domains, whitelist, teamWhitelist or AllowAllUsers configured, any successful auth to the IdP authorizes access") + return true, nil + } +} + +// CallbackHandler /auth +// - 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) { + log.Debug("/auth") + // Handle the exchange code to initiate a transport. + + session, err := sessstore.Get(r, cfg.Cfg.Session.Name) + if err != nil { + log.Errorf("/auth could not find session store %s", cfg.Cfg.Session.Name) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // is the nonce "state" valid? + queryState := r.URL.Query().Get("state") + if session.Values["state"] != queryState { + log.Errorf("/auth Invalid session state: stored %s, returned %s", session.Values["state"], queryState) + renderIndex(w, "/auth Invalid session state.") + return + } + + errorState := r.URL.Query().Get("error") + if errorState != "" { + errorDescription := r.URL.Query().Get("error_description") + log.Warn("/auth Error state: ", errorState, ", Error description: ", errorDescription) + w.WriteHeader(http.StatusForbidden) + renderIndex(w, "FORBIDDEN: "+errorDescription) + return + } + + user := structs.User{} + customClaims := structs.CustomClaims{} + ptokens := structs.PTokens{} + + if err := getUserInfo(r, &user, &customClaims, &ptokens); err != nil { + log.Error(err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + log.Debugf("/auth Claims from userinfo: %+v", customClaims) + //getProviderJWT(r, &user) + log.Debug("/auth CallbackHandler") + log.Debugf("/auth %+v", user) + + if ok, err := VerifyUser(user); !ok { + log.Error(err) + renderIndex(w, fmt.Sprintf("/auth User is not authorized. %s Please try again.", err)) + return + } + + // SUCCESS!! they are authorized + + // issue the jwt + tokenstring := jwtmanager.CreateUserTokenString(user, customClaims, ptokens) + cookie.SetCookie(w, r, tokenstring) + + // get the originally requested URL so we can send them on their way + requestedURL := session.Values["requestedURL"].(string) + if requestedURL != "" { + // clear out the session value + session.Values["requestedURL"] = "" + session.Values[requestedURL] = 0 + if err = session.Save(r, w); err != nil { + log.Error(err) + } + + redirect302(w, r, requestedURL) + return + } + // otherwise serve an html page + renderIndex(w, "/auth "+tokenstring) +} + +func getUserInfo(r *http.Request, user *structs.User, customClaims *structs.CustomClaims, ptokens *structs.PTokens) error { + return provider.GetUserInfo(r, user, customClaims, ptokens) +} + +func getProvider() Provider { + switch cfg.GenOAuth.Provider { + case cfg.Providers.IndieAuth: + return indieauth.Provider{} + case cfg.Providers.ADFS: + return adfs.Provider{} + case cfg.Providers.HomeAssistant: + return homeassistant.Provider{} + case cfg.Providers.OpenStax: + return openstax.Provider{} + case cfg.Providers.Google: + return google.Provider{} + case cfg.Providers.GitHub: + return github.Provider{PrepareTokensAndClient: common.PrepareTokensAndClient} + case cfg.Providers.Nextcloud: + return nextcloud.Provider{} + case cfg.Providers.OIDC: + return openid.Provider{} + default: + // shouldn't ever reach this since cfg checks for a properly configure `oauth.provider` + log.Fatal("oauth.provider appears to be misconfigured, please check your config") + 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-Vouch-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 { + cfg.Cfg.TestURLs = append(cfg.Cfg.TestURLs, rURL) + renderIndex(w, "302 redirect to: "+rURL) + return + } + http.Redirect(w, r, rURL, http.StatusFound) +} + +func ok200(w http.ResponseWriter, r *http.Request) { + _, err := w.Write([]byte("200 OK\n")) + if err != nil { + log.Error(err) + } +} diff --git a/handlers/handlers_test.go b/handlers/handlers_test.go index 93495492..1a5838c4 100644 --- a/handlers/handlers_test.go +++ b/handlers/handlers_test.go @@ -1,13 +1,3 @@ -/* - -Copyright 2020 The Vouch Proxy Authors. -Use of this source code is governed by The MIT License (MIT) that -can be found in the LICENSE file. Software distributed under The -MIT License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES -OR CONDITIONS OF ANY KIND, either express or implied. - -*/ - package handlers import ( @@ -58,6 +48,12 @@ func setUp(configFile string) { // } func TestValidateRequestHandlerWithGroupClaims(t *testing.T) { + // setUp() + + // user := structs.User{ + // Username: "test@testing.com", + // Name: "Test Name", + // } setUp("/config/testing/handler_claims.yml") customClaims := structs.CustomClaims{ @@ -71,17 +67,9 @@ func TestValidateRequestHandlerWithGroupClaims(t *testing.T) { "family_name": "Tester", "email": "mrtester@test.int", "boolean_claim": true, - // Auth0 custom claim are URLs - // https://auth0.com/docs/tokens/guides/create-namespaced-custom-claims - "http://www.example.com/favorite_color": "blue", }, } - groupHeader := "X-Vouch-IdP-Claims-Groups" - booleanHeader := "X-Vouch-IdP-Claims-Boolean-Claim" - familyNameHeader := "X-Vouch-IdP-Claims-Family-Name" - favoriteColorHeader := "X-Vouch-IdP-Claims-Www-Example-Com-Favorite-Color" - tokens := structs.PTokens{ // PAccessToken: "eyJhbGciOiJSUzI1NiIsImtpZCI6IjRvaXU4In0.eyJzdWIiOiJuZnlmZSIsImF1ZCI6ImltX29pY19jbGllbnQiLCJqdGkiOiJUOU4xUklkRkVzUE45enU3ZWw2eng2IiwiaXNzIjoiaHR0cHM6XC9cL3Nzby5tZXljbG91ZC5uZXQ6OTAzMSIsImlhdCI6MTM5MzczNzA3MSwiZXhwIjoxMzkzNzM3MzcxLCJub25jZSI6ImNiYTU2NjY2LTRiMTItNDU2YS04NDA3LTNkMzAyM2ZhMTAwMiIsImF0X2hhc2giOiJrdHFvZVBhc2praVY5b2Z0X3o5NnJBIn0.g1Jc9DohWFfFG3ppWfvW16ib6YBaONC5VMs8J61i5j5QLieY-mBEeVi1D3vr5IFWCfivY4hZcHtoJHgZk1qCumkAMDymsLGX-IGA7yFU8LOjUdR4IlCPlZxZ_vhqr_0gQ9pCFKDkiOv1LVv5x3YgAdhHhpZhxK6rWxojg2RddzvZ9Xi5u2V1UZ0jukwyG2d4PRzDn7WoRNDGwYOEt4qY7lv_NO2TY2eAklP-xYBWu0b9FBElapnstqbZgAXdndNs-Wqp4gyQG5D0owLzxPErR9MnpQfgNcai-PlWI_UrvoopKNbX0ai2zfkuQ-qh6Xn8zgkiaYDHzq4gzwRfwazaqA", // PIdToken: "eyJhbGciOiJSUzI1NiIsImtpZCI6IjRvaXU4In0.eyJzdWIiOiJuZnlmZSIsImF1ZCI6ImltX29pY19jbGllbnQiLCJqdGkiOiJUOU4xUklkRkVzUE45enU3ZWw2eng2IiwiaXNzIjoiaHR0cHM6XC9cL3Nzby5tZXljbG91ZC5uZXQ6OTAzMSIsImlhdCI6MTM5MzczNzA3MSwiZXhwIjoxMzkzNzM3MzcxLCJub25jZSI6ImNiYTU2NjY2LTRiMTItNDU2YS04NDA3LTNkMzAyM2ZhMTAwMiIsImF0X2hhc2giOiJrdHFvZVBhc2praVY5b2Z0X3o5NnJBIn0.g1Jc9DohWFfFG3ppWfvW16ib6YBaONC5VMs8J61i5j5QLieY-mBEeVi1D3vr5IFWCfivY4hZcHtoJHgZk1qCumkAMDymsLGX-IGA7yFU8LOjUdR4IlCPlZxZ_vhqr_0gQ9pCFKDkiOv1LVv5x3YgAdhHhpZhxK6rWxojg2RddzvZ9Xi5u2V1UZ0jukwyG2d4PRzDn7WoRNDGwYOEt4qY7lv_NO2TY2eAklP-xYBWu0b9FBElapnstqbZgAXdndNs-Wqp4gyQG5D0owLzxPErR9MnpQfgNcai-PlWI_UrvoopKNbX0ai2zfkuQ-qh6Xn8zgkiaYDHzq4gzwRfwazaqA", @@ -112,12 +100,15 @@ func TestValidateRequestHandlerWithGroupClaims(t *testing.T) { status, http.StatusOK) } + groupHeader := "X-Vouch-IdP-Claims-Groups" + booleanHeader := "X-Vouch-IdP-Claims-Boolean-Claim" + familyNameHeader := "X-Vouch-IdP-Claims-Family-Name" + // Check that the custom claim headers are what we expected customClaimHeaders := map[string][]string{ - strings.ToLower(groupHeader): []string{}, - strings.ToLower(booleanHeader): []string{}, - strings.ToLower(familyNameHeader): []string{}, - strings.ToLower(favoriteColorHeader): []string{}, + strings.ToLower(groupHeader): []string{}, + strings.ToLower(booleanHeader): []string{}, + strings.ToLower(familyNameHeader): []string{}, } for k, v := range rr.Result().Header { @@ -127,10 +118,9 @@ func TestValidateRequestHandlerWithGroupClaims(t *testing.T) { } } expectedCustomClaimHeaders := map[string][]string{ - strings.ToLower(groupHeader): []string{"\"Website Users\",\"Test Group\""}, - strings.ToLower(booleanHeader): []string{"true"}, - strings.ToLower(familyNameHeader): []string{"Tester"}, - strings.ToLower(favoriteColorHeader): []string{"blue"}, + strings.ToLower(groupHeader): []string{"\"Website Users\",\"Test Group\""}, + strings.ToLower(booleanHeader): []string{"true"}, + strings.ToLower(familyNameHeader): []string{"Tester"}, } assert.Equal(t, expectedCustomClaimHeaders, customClaimHeaders) } @@ -138,7 +128,7 @@ func TestValidateRequestHandlerWithGroupClaims(t *testing.T) { func TestVerifyUserPositiveUserInWhiteList(t *testing.T) { setUp("/config/testing/handler_whitelist.yml") user := &structs.User{Username: "test@example.com", Email: "test@example.com", Name: "Test Name"} - ok, err := verifyUser(*user) + ok, err := VerifyUser(*user) assert.True(t, ok) assert.Nil(t, err) } @@ -156,7 +146,7 @@ func TestVerifyUserPositiveAllowAllUsers(t *testing.T) { user := &structs.User{Username: "testuser", Email: "test@example.com", Name: "Test Name"} - ok, err := verifyUser(*user) + ok, err := VerifyUser(*user) assert.True(t, ok) assert.Nil(t, err) } @@ -164,7 +154,7 @@ func TestVerifyUserPositiveAllowAllUsers(t *testing.T) { func TestVerifyUserPositiveByEmail(t *testing.T) { setUp("/config/testing/handler_email.yml") user := &structs.User{Username: "testuser", Email: "test@example.com", Name: "Test Name"} - ok, err := verifyUser(*user) + ok, err := VerifyUser(*user) assert.True(t, ok) assert.Nil(t, err) } @@ -176,7 +166,7 @@ func TestVerifyUserPositiveByTeam(t *testing.T) { user := &structs.User{Username: "testuser", Email: "test@example.com", Name: "Test Name"} user.TeamMemberships = append(user.TeamMemberships, "org1/team3") user.TeamMemberships = append(user.TeamMemberships, "org1/team1") - ok, err := verifyUser(*user) + ok, err := VerifyUser(*user) assert.True(t, ok) assert.Nil(t, err) } @@ -186,7 +176,7 @@ func TestVerifyUserNegativeByTeam(t *testing.T) { user := &structs.User{Username: "testuser", Email: "test@example.com", Name: "Test Name"} // cfg.Cfg.TeamWhiteList = append(cfg.Cfg.TeamWhiteList, "org1/team1") - ok, err := verifyUser(*user) + ok, err := VerifyUser(*user) assert.False(t, ok) assert.NotNil(t, err) } @@ -196,7 +186,7 @@ func TestVerifyUserPositiveNoDomainsConfigured(t *testing.T) { user := &structs.User{Username: "testuser", Email: "test@example.com", Name: "Test Name"} cfg.Cfg.Domains = make([]string, 0) - ok, err := verifyUser(*user) + ok, err := VerifyUser(*user) assert.True(t, ok) assert.Nil(t, err) @@ -205,7 +195,7 @@ func TestVerifyUserPositiveNoDomainsConfigured(t *testing.T) { func TestVerifyUserNegative(t *testing.T) { setUp("/config/testing/test_config.yml") user := &structs.User{Username: "testuser", Email: "test@example.com", Name: "Test Name"} - ok, err := verifyUser(*user) + ok, err := VerifyUser(*user) assert.False(t, ok) assert.NotNil(t, err) diff --git a/handlers/healthcheck.go b/handlers/healthcheck.go deleted file mode 100644 index ca9050f0..00000000 --- a/handlers/healthcheck.go +++ /dev/null @@ -1,25 +0,0 @@ -/* - -Copyright 2020 The Vouch Proxy Authors. -Use of this source code is governed by The MIT License (MIT) that -can be found in the LICENSE file. Software distributed under The -MIT License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES -OR CONDITIONS OF ANY KIND, either express or implied. - -*/ - -package handlers - -import ( - "fmt" - "net/http" -) - -// HealthcheckHandler /healthcheck -// just returns 200 '{ "ok": true }' -func HealthcheckHandler(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - if _, err := fmt.Fprintf(w, "{ \"ok\": true }"); err != nil { - log.Error(err) - } -} diff --git a/handlers/homeassistant/homeassistant.go b/handlers/homeassistant/homeassistant.go index 4f7a2833..adfbb1cf 100644 --- a/handlers/homeassistant/homeassistant.go +++ b/handlers/homeassistant/homeassistant.go @@ -1,13 +1,3 @@ -/* - -Copyright 2020 The Vouch Proxy Authors. -Use of this source code is governed by The MIT License (MIT) that -can be found in the LICENSE file. Software distributed under The -MIT License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES -OR CONDITIONS OF ANY KIND, either express or implied. - -*/ - package homeassistant import ( diff --git a/handlers/indieauth/indieauth.go b/handlers/indieauth/indieauth.go index 397a13fb..a56645db 100644 --- a/handlers/indieauth/indieauth.go +++ b/handlers/indieauth/indieauth.go @@ -1,13 +1,3 @@ -/* - -Copyright 2020 The Vouch Proxy Authors. -Use of this source code is governed by The MIT License (MIT) that -can be found in the LICENSE file. Software distributed under The -MIT License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES -OR CONDITIONS OF ANY KIND, either express or implied. - -*/ - package indieauth import ( diff --git a/handlers/login.go b/handlers/login.go deleted file mode 100644 index 664de344..00000000 --- a/handlers/login.go +++ /dev/null @@ -1,193 +0,0 @@ -/* - -Copyright 2020 The Vouch Proxy Authors. -Use of this source code is governed by The MIT License (MIT) that -can be found in the LICENSE file. Software distributed under The -MIT License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES -OR CONDITIONS OF ANY KIND, either express or implied. - -*/ - -package handlers - -import ( - "errors" - "fmt" - "net/http" - "net/url" - "regexp" - "strings" - - "github.com/theckman/go-securerandom" - "github.com/vouch/vouch-proxy/pkg/cfg" - "github.com/vouch/vouch-proxy/pkg/cookie" - "github.com/vouch/vouch-proxy/pkg/domains" - "golang.org/x/oauth2" -) - -var errTooManyRedirects = errors.New("too many redirects for requested URL") - -// LoginHandler /login -// currently performs a 302 redirect to Google -func LoginHandler(w http.ResponseWriter, r *http.Request) { - log.Debug("/login") - // no matter how you ended up here, make sure the cookie gets cleared out - cookie.ClearCookie(w, r) - - session, err := sessstore.Get(r, cfg.Cfg.Session.Name) - if err != nil { - log.Infof("couldn't find existing encrypted secure cookie with name %s: %s (probably fine)", cfg.Cfg.Session.Name, err) - } - - state, err := generateStateNonce() - if err != nil { - log.Error(err) - } - - // set the state variable in the session - session.Values["state"] = state - log.Debugf("session state set to %s", session.Values["state"]) - - // requestedURL comes from nginx in the query string via a 302 redirect - // it sets the ultimate destination - // https://vouch.yoursite.com/login?url= - // need to clean the URL to prevent malicious redirection - var requestedURL string - if requestedURL, err = getValidRequestedURL(r); err != nil { - error400(w, r, err) - return - } - - // set session variable for eventual 302 redirecton to original request - session.Values["requestedURL"] = requestedURL - log.Debugf("session requestedURL set to %s", session.Values["requestedURL"]) - - // increment the failure counter for the requestedURL - // stop them after three failures for this URL - var failcount = 0 - if session.Values[requestedURL] != nil { - failcount = session.Values[requestedURL].(int) - log.Debugf("failcount for %s is %d", requestedURL, failcount) - } - failcount++ - session.Values[requestedURL] = failcount - - log.Debugf("saving session with failcount %d", failcount) - if err = session.Save(r, w); err != nil { - log.Error(err) - } - - if failcount > 2 { - var vouchError = r.URL.Query().Get("error") - error400(w, r, fmt.Errorf("/login %w for %s - %s", errTooManyRedirects, requestedURL, vouchError)) - return - } - - // SUCCESS - // bounce to oauth provider for login - var lURL = loginURL(r, state) - log.Debugf("redirecting to oauthURL %s", lURL) - redirect302(w, r, lURL) -} - -var ( - errNoURL = errors.New("no destination URL requested") - errInvalidURL = errors.New("requested destination URL appears to be invalid") - errURLNotHTTP = errors.New("requested destination URL is not a valid URL (does not begin with 'http://' or 'https://')") - errDangerQS = errors.New("requested destination URL has a dangerous query string") - badStrings = []string{"http://", "https://", "data:", "ftp://", "ftps://", "//", "javascript:"} -) - -func getValidRequestedURL(r *http.Request) (string, error) { - urlparam := r.URL.Query().Get("url") - - if urlparam == "" { - return "", errNoURL - } - if !strings.HasPrefix(strings.ToLower(urlparam), "http://") && !strings.HasPrefix(strings.ToLower(urlparam), "https://") { - return "", errURLNotHTTP - } - u, err := url.Parse(urlparam) - if err != nil { - return "", fmt.Errorf("won't parse: %w %s", errInvalidURL, err) - } - - _, err = url.ParseQuery(u.RawQuery) - if err != nil { - return "", fmt.Errorf("query string won't parse: %w %s", errInvalidURL, err) - } - - for _, v := range u.Query() { - // log.Debugf("validateRequestedURL %s:%s", k, v) - for _, vval := range v { - for _, bad := range badStrings { - if strings.HasPrefix(strings.ToLower(vval), bad) { - return "", fmt.Errorf("%w looks bad: %s includes %s", errDangerQS, vval, bad) - } - } - } - } - - hostname := u.Hostname() - if cfg.GenOAuth.Provider != cfg.Providers.IndieAuth { - d := domains.Matches(hostname) - if d == "" { - if cfg.Cfg.Cookie.Domain == "" || !strings.Contains(hostname, cfg.Cfg.Cookie.Domain) { - return "", fmt.Errorf("%w: not within a %s managed domain", errInvalidURL, cfg.Branding.FullName) - } - } - } - - // if the requested URL is http then the cookie cannot be seen if cfg.Cfg.Cookie.Secure is set - if u.Scheme == "http" && cfg.Cfg.Cookie.Secure { - return "", fmt.Errorf("%w: mismatch between requested destination URL and %s.cookie.secure %v (the cookie will not be visible to https)", errInvalidURL, cfg.Branding.LCName, cfg.Cfg.Cookie.Secure) - } - - // and irregardless cookies placed from https are not able to be seen by http - // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#Secure - if u.Scheme != r.URL.Scheme { - log.Warnf("the requested destination URL %s is %s but %s is running under %s, this may mean the jwt/cookie cannot be seen in some browsers", u, u.Scheme, cfg.Branding.FullName, r.URL.Scheme) - } - - return urlparam, nil -} - -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 lurl = "" - - if cfg.GenOAuth.Provider == cfg.Providers.IndieAuth { - lurl = cfg.OAuthClient.AuthCodeURL(state, oauth2.SetAuthURLParam("response_type", "id")) - } else if cfg.GenOAuth.Provider == cfg.Providers.ADFS { - lurl = cfg.OAuthClient.AuthCodeURL(state, cfg.OAuthopts) - } else { - domain := domains.Matches(r.Host) - log.Debugf("looking for callback_url matching %v", domain) - for i, v := range cfg.GenOAuth.RedirectURLs { - if strings.Contains(v, domain) { - log.Debugf("redirect value matched at [%d]=%v", i, v) - cfg.OAuthClient.RedirectURL = v - break - } - } - if cfg.OAuthopts != nil { - lurl = cfg.OAuthClient.AuthCodeURL(state, cfg.OAuthopts) - } else { - lurl = cfg.OAuthClient.AuthCodeURL(state) - } - } - // log.Debugf("loginURL %s", url) - return lurl -} - -var regExJustAlphaNum, _ = regexp.Compile("[^a-zA-Z0-9]+") - -func generateStateNonce() (string, error) { - state, err := securerandom.URLBase64InBytes(base64Bytes) - if err != nil { - return "", err - } - state = regExJustAlphaNum.ReplaceAllString(state, "") - return state, nil -} diff --git a/handlers/login_test.go b/handlers/login_test.go deleted file mode 100644 index 831ba155..00000000 --- a/handlers/login_test.go +++ /dev/null @@ -1,53 +0,0 @@ -/* - -Copyright 2020 The Vouch Proxy Authors. -Use of this source code is governed by The MIT License (MIT) that -can be found in the LICENSE file. Software distributed under The -MIT License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES -OR CONDITIONS OF ANY KIND, either express or implied. - -*/ - -package handlers - -import ( - "net/http" - "net/url" - "testing" -) - -func Test_getValidRequestedURL(t *testing.T) { - setUp("/config/testing/handler_login_url.yml") - r := &http.Request{} - tests := []struct { - name string - url string - want string - wantErr bool - }{ - {"no https", "example.com/dest", "", true}, - {"redirection chaining", "http://example.com/dest?url=https://", "", true}, - {"redirection chaining upper case", "http://example.com/dest?url=HTTPS://someplaceelse.com", "", true}, - {"redirection chaining no protocol", "http://example.com/dest?url=//someplaceelse.com", "", true}, - {"data uri", "http://example.com/dest?url=data:text/plain,Example+Text", "", true}, - {"javascript uri", "http://example.com/dest?url=javascript:alert(1)", "", true}, - {"not in domain", "http://somewherelse.com/", "", true}, - {"should warn", "https://example.com/", "https://example.com/", false}, - {"should be fine", "http://example.com/", "http://example.com/", false}, - - // TODO: Add test cases. - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - r.URL, _ = url.Parse("http://vouch.example.com/login?url=" + tt.url) - got, err := getValidRequestedURL(r) - if (err != nil) != tt.wantErr { - t.Errorf("getValidRequestedURL() error = %v, wantErr %v", err, tt.wantErr) - return - } - if got != tt.want { - t.Errorf("getValidRequestedURL() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/handlers/logout.go b/handlers/logout.go deleted file mode 100644 index 25d4519e..00000000 --- a/handlers/logout.go +++ /dev/null @@ -1,53 +0,0 @@ -/* - -Copyright 2020 The Vouch Proxy Authors. -Use of this source code is governed by The MIT License (MIT) that -can be found in the LICENSE file. Software distributed under The -MIT License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES -OR CONDITIONS OF ANY KIND, either express or implied. - -*/ - -package handlers - -import ( - "fmt" - "net/http" - - "github.com/vouch/vouch-proxy/pkg/cfg" - "github.com/vouch/vouch-proxy/pkg/cookie" -) - -var errUnauthRedirURL = fmt.Errorf("/logout The requested url is not present in `%s.post_logout_redirect_uris`", cfg.Branding.LCName) - -// LogoutHandler /logout -// 302 redirect to the provider -func LogoutHandler(w http.ResponseWriter, r *http.Request) { - log.Debug("/logout") - - cookie.ClearCookie(w, r) - log.Debug("/logout deleting session") - sessstore.MaxAge(-1) - session, err := sessstore.Get(r, cfg.Cfg.Session.Name) - if err != nil { - log.Error(err) - } - if err = session.Save(r, w); err != nil { - log.Error(err) - } - sessstore.MaxAge(300) - - var requestedURL = r.URL.Query().Get("url") - if requestedURL != "" { - for _, allowed := range cfg.Cfg.LogoutRedirectURLs { - if allowed == requestedURL { - log.Debugf("/logout found ") - redirect302(w, r, allowed) - return - } - } - error400(w, r, fmt.Errorf("%w: %s", errUnauthRedirURL, requestedURL)) - return - } - renderIndex(w, "/logout you have been logged out") -} diff --git a/handlers/logout_test.go b/handlers/logout_test.go deleted file mode 100644 index 11eab4de..00000000 --- a/handlers/logout_test.go +++ /dev/null @@ -1,47 +0,0 @@ -/* - -Copyright 2020 The Vouch Proxy Authors. -Use of this source code is governed by The MIT License (MIT) that -can be found in the LICENSE file. Software distributed under The -MIT License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES -OR CONDITIONS OF ANY KIND, either express or implied. - -*/ - -package handlers - -import ( - "net/http" - "net/http/httptest" - "testing" -) - -func TestLogoutHandler(t *testing.T) { - setUp("/config/testing/handler_logout_url.yml") - handler := http.HandlerFunc(LogoutHandler) - - tests := []struct { - name string - url string - wantcode int - }{ - {"allowed", "http://myapp.example.com/login", http.StatusFound}, - {"allowed", "https://oauth2.googleapis.com/revoke", http.StatusFound}, - {"not allowed", "http://myapp.example.com/loginagain", http.StatusBadRequest}, - {"not allowed", "http://google.com/", http.StatusBadRequest}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - req, err := http.NewRequest("GET", "/logout?url="+tt.url, nil) - if err != nil { - t.Fatal(err) - } - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - if rr.Code != tt.wantcode { - t.Errorf("LogoutHandler() = %v, want %v", rr.Code, tt.wantcode) - } - }) - } -} diff --git a/handlers/nextcloud/nextcloud.go b/handlers/nextcloud/nextcloud.go index 6316c0ec..6e3ff3e7 100644 --- a/handlers/nextcloud/nextcloud.go +++ b/handlers/nextcloud/nextcloud.go @@ -1,13 +1,3 @@ -/* - -Copyright 2020 The Vouch Proxy Authors. -Use of this source code is governed by The MIT License (MIT) that -can be found in the LICENSE file. Software distributed under The -MIT License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES -OR CONDITIONS OF ANY KIND, either express or implied. - -*/ - package nextcloud import ( diff --git a/handlers/openid/openid.go b/handlers/openid/openid.go index 4d6f22f8..e6b109b8 100644 --- a/handlers/openid/openid.go +++ b/handlers/openid/openid.go @@ -1,13 +1,3 @@ -/* - -Copyright 2020 The Vouch Proxy Authors. -Use of this source code is governed by The MIT License (MIT) that -can be found in the LICENSE file. Software distributed under The -MIT License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES -OR CONDITIONS OF ANY KIND, either express or implied. - -*/ - package openid import ( diff --git a/handlers/openstax/openstax.go b/handlers/openstax/openstax.go index b8d671f1..bd217999 100644 --- a/handlers/openstax/openstax.go +++ b/handlers/openstax/openstax.go @@ -1,13 +1,3 @@ -/* - -Copyright 2020 The Vouch Proxy Authors. -Use of this source code is governed by The MIT License (MIT) that -can be found in the LICENSE file. Software distributed under The -MIT License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES -OR CONDITIONS OF ANY KIND, either express or implied. - -*/ - package openstax import ( diff --git a/handlers/responses.go b/handlers/responses.go deleted file mode 100644 index 1d568f36..00000000 --- a/handlers/responses.go +++ /dev/null @@ -1,62 +0,0 @@ -/* - -Copyright 2020 The Vouch Proxy Authors. -Use of this source code is governed by The MIT License (MIT) that -can be found in the LICENSE file. Software distributed under The -MIT License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES -OR CONDITIONS OF ANY KIND, either express or implied. - -*/ - -package handlers - -import ( - "fmt" - "net/http" - - "github.com/vouch/vouch-proxy/pkg/cfg" - "github.com/vouch/vouch-proxy/pkg/cookie" -) - -func renderIndex(w http.ResponseWriter, msg string) { - if err := indexTemplate.Execute(w, &Index{Msg: msg, TestURLs: cfg.Cfg.TestURLs, Testing: cfg.Cfg.Testing}); err != nil { - log.Error(err) - } -} -func ok200(w http.ResponseWriter, r *http.Request) { - _, err := w.Write([]byte("200 OK\n")) - if err != nil { - log.Error(err) - } -} - -func redirect302(w http.ResponseWriter, r *http.Request, rURL string) { - if cfg.Cfg.Testing { - cfg.Cfg.TestURLs = append(cfg.Cfg.TestURLs, rURL) - renderIndex(w, "302 redirect to: "+rURL) - return - } - http.Redirect(w, r, rURL, http.StatusFound) -} - -// 400 Bad Request -// returned when the requesed url param for /login or /logout is bd -func error400(w http.ResponseWriter, r *http.Request, e error) { - log.Error(e) - cookie.ClearCookie(w, r) - w.Header().Set("X-Vouch-Error", e.Error()) - http.Error(w, e.Error(), http.StatusBadRequest) -} - -// 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, e error) { - log.Error(e) - cookie.ClearCookie(w, r) - w.Header().Set("X-Vouch-Error", e.Error()) - http.Error(w, e.Error(), http.StatusUnauthorized) -} - -func error401na(w http.ResponseWriter, r *http.Request) { - error401(w, r, fmt.Errorf("not authorized")) -} diff --git a/handlers/validate.go b/handlers/validate.go deleted file mode 100644 index 9569f0ba..00000000 --- a/handlers/validate.go +++ /dev/null @@ -1,184 +0,0 @@ -/* - -Copyright 2020 The Vouch Proxy Authors. -Use of this source code is governed by The MIT License (MIT) that -can be found in the LICENSE file. Software distributed under The -MIT License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES -OR CONDITIONS OF ANY KIND, either express or implied. - -*/ - -package handlers - -import ( - "errors" - "fmt" - "net/http" - "reflect" - "strings" - - "github.com/vouch/vouch-proxy/pkg/cfg" - "github.com/vouch/vouch-proxy/pkg/cookie" - "github.com/vouch/vouch-proxy/pkg/jwtmanager" - "go.uber.org/zap" -) - -var ( - errNoJWT = errors.New("no jwt found in request") - errNoUser = errors.New("no User found in jwt") -) - -// ValidateRequestHandler /validate -// TODO this should use the handler interface -func ValidateRequestHandler(w http.ResponseWriter, r *http.Request) { - fastlog.Debug("/validate") - - jwt := findJWT(r) - if jwt == "" { - send401or200PublicAccess(w, r, errNoJWT) - return - } - - claims, err := claimsFromJWT(jwt) - if err != nil { - send401or200PublicAccess(w, r, err) - return - } - - if claims.Username == "" { - send401or200PublicAccess(w, r, errNoUser) - return - } - - if !cfg.Cfg.AllowAllUsers { - if !jwtmanager.SiteInClaims(r.Host, &claims) { - send401or200PublicAccess(w, r, - fmt.Errorf("http header 'Host: %s' not authorized for configured `vouch.domains` (is Host being sent properly?)", r.Host)) - return - } - } - - generateCustomClaimsHeaders(w, claims) - w.Header().Add(cfg.Cfg.Headers.User, claims.Username) - - w.Header().Add(cfg.Cfg.Headers.Success, "true") - - if cfg.Cfg.Headers.AccessToken != "" && claims.PAccessToken != "" { - w.Header().Add(cfg.Cfg.Headers.AccessToken, claims.PAccessToken) - } - if cfg.Cfg.Headers.IDToken != "" && claims.PIdToken != "" { - w.Header().Add(cfg.Cfg.Headers.IDToken, claims.PIdToken) - } - // fastlog.Debugf("response headers %+v", w.Header()) - // fastlog.Debug("response header", - // zap.String(cfg.Cfg.Headers.User, w.Header().Get(cfg.Cfg.Headers.User))) - fastlog.Debug("response header", - zap.Any("all headers", w.Header())) - - // good to go!! - 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 - -} - -func generateCustomClaimsHeaders(w http.ResponseWriter, claims jwtmanager.VouchClaims) { - if len(cfg.Cfg.Headers.ClaimsCleaned) > 0 { - log.Debug("Found claims in config, finding specific keys...") - // Run through all the claims found - for k, v := range claims.CustomClaims { - // Run through the claims we are looking for - for claim, header := range cfg.Cfg.Headers.ClaimsCleaned { - // Check for matching claim - if claim == k { - log.Debugf("Found matching claim key: %s", k) - if val, ok := v.([]interface{}); ok { - strs := make([]string, len(val)) - for i, v := range val { - strs[i] = fmt.Sprintf("\"%s\"", v) - } - log.Debugf("Adding header for claim %s - %s: %s", k, header, val) - w.Header().Add(header, strings.Join(strs, ",")) - } else { - // convert to string - val := fmt.Sprint(v) - if reflect.TypeOf(val).Kind() == reflect.String { - // if val, ok := v.(string); ok { - w.Header().Add(header, val) - log.Debugf("Adding header for claim %s - %s: %s", k, header, val) - } else { - log.Errorf("Couldn't parse header type for %s %+v. Please submit an issue.", k, v) - } - } - } - } - } - } - -} - -func send401or200PublicAccess(w http.ResponseWriter, r *http.Request, e error) { - if cfg.Cfg.PublicAccess { - log.Debugf("error: %s, but public access is '%v', returning ok200", e, cfg.Cfg.PublicAccess) - w.Header().Add(cfg.Cfg.Headers.User, "") - ok200(w, r) - return - } - error401(w, r, e) -} - -// 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) - 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 - } - } - 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 "" -} - -// claimsFromJWT parse the jwt and return the claims -func claimsFromJWT(jwt string) (jwtmanager.VouchClaims, error) { - var claims jwtmanager.VouchClaims - - jwtParsed, err := jwtmanager.ParseTokenString(jwt) - if err != nil { - // it didn't parse, which means its bad, start over - log.Error("jwtParsed returned error, clearing cookie") - return claims, err - } - - claims, err = jwtmanager.PTokenClaims(jwtParsed) - if err != nil { - // claims = jwtmanager.PTokenClaims(jwtParsed) - // if claims == &jwtmanager.VouchClaims{} { - return claims, err - } - log.Debugf("JWT Claims: %+v", claims) - return claims, nil -} diff --git a/main.go b/main.go index dd0585f9..015c556b 100644 --- a/main.go +++ b/main.go @@ -1,27 +1,8 @@ -/* - -Copyright 2020 The Vouch Proxy Authors. -Use of this source code is governed by The MIT License (MIT) that -can be found in the LICENSE file. Software distributed under The -MIT License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES -OR CONDITIONS OF ANY KIND, either express or implied. - -*/ - package main -// Vouch Proxy +// vouch // github.com/vouch/vouch-proxy -/* - -Hello Developer! Thanks for looking at the code! - -Before submitting PRs, please see the README... -https://github.com/vouch/vouch-proxy#submitting-a-pull-request-for-a-new-feature - -*/ - import ( "errors" "flag" @@ -33,16 +14,19 @@ import ( "strconv" "time" + "github.com/vouch/vouch-proxy/pkg/healthcheck" + "github.com/vouch/vouch-proxy/pkg/response" + + "github.com/vouch/vouch-proxy/pkg/cookie" + "github.com/vouch/vouch-proxy/pkg/jwtmanager" + + "github.com/vouch/vouch-proxy/pkg/domains" + "github.com/gorilla/mux" "go.uber.org/zap" "github.com/vouch/vouch-proxy/handlers" "github.com/vouch/vouch-proxy/pkg/cfg" - "github.com/vouch/vouch-proxy/pkg/cookie" - "github.com/vouch/vouch-proxy/pkg/domains" - "github.com/vouch/vouch-proxy/pkg/healthcheck" - "github.com/vouch/vouch-proxy/pkg/jwtmanager" - "github.com/vouch/vouch-proxy/pkg/response" "github.com/vouch/vouch-proxy/pkg/timelog" ) @@ -108,7 +92,7 @@ func main() { var listen = cfg.Cfg.Listen + ":" + strconv.Itoa(cfg.Cfg.Port) checkTCPPortAvailable(listen) - logger.Infow("starting "+cfg.Branding.FullName, + logger.Infow("starting "+cfg.Branding.CcName, // "semver": semver, "version", version, "buildtime", builddt, @@ -165,7 +149,7 @@ func checkTCPPortAvailable(listen string) { conn, err := net.Listen("tcp", listen) if err != nil { logger.Error(err) - logger.Fatal(errors.New(listen + " is not available (is " + cfg.Branding.FullName + " already running?)")) + logger.Fatal(errors.New(listen + " is not available (is " + cfg.Branding.CcName + " already running?)")) } if err = conn.Close(); err != nil { logger.Error(err) diff --git a/pkg/cfg/cfg.go b/pkg/cfg/cfg.go index 307685dc..c5721d9c 100644 --- a/pkg/cfg/cfg.go +++ b/pkg/cfg/cfg.go @@ -1,13 +1,3 @@ -/* - -Copyright 2020 The Vouch Proxy Authors. -Use of this source code is governed by The MIT License (MIT) that -can be found in the LICENSE file. Software distributed under The -MIT License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES -OR CONDITIONS OF ANY KIND, either express or implied. - -*/ - package cfg import ( @@ -20,8 +10,6 @@ import ( "regexp" "strings" - "github.com/mitchellh/mapstructure" - "golang.org/x/oauth2" "github.com/spf13/viper" @@ -72,10 +60,9 @@ type Config struct { Name string `mapstructure:"name"` Key string `mapstructure:"key"` } - TestURL string `mapstructure:"test_url"` - TestURLs []string `mapstructure:"test_urls"` - Testing bool `mapstructure:"testing"` - LogoutRedirectURLs []string `mapstructure:"post_logout_redirect_uris"` + TestURL string `mapstructure:"test_url"` + TestURLs []string `mapstructure:"test_urls"` + Testing bool `mapstructure:"testing"` } // oauth config items endoint for access @@ -110,14 +97,13 @@ type branding struct { LCName string // lower case UCName string // upper case CcName string // camel case - FullName string // Vouch Proxy OldLCName string // lasso URL string // https://github.com/vouch/vouch-proxy } var ( // Branding that's our name - Branding = branding{"vouch", "VOUCH", "Vouch", "Vouch Proxy", "lasso", "https://github.com/vouch/vouch-proxy"} + Branding = branding{"vouch", "VOUCH", "Vouch", "lasso", "https://github.com/vouch/vouch-proxy"} // GenOAuth exported OAuth config variable // TODO: I think GenOAuth and OAuthConfig can be combined! @@ -209,7 +195,7 @@ func Configure() { // TestConfiguration Confirm the Configuration is valid func TestConfiguration() { if Cfg.Testing { - // Logging.setLogLevel(zap.DebugLevel) + Logging.setLogLevel(zap.DebugLevel) Logging.setDevelopmentLogger() } @@ -292,17 +278,9 @@ func parseConfig() { log.Fatalf("Fatal error config file: %s", err.Error()) log.Panic(err) } - - if err = checkConfigFileWellFormed(); err != nil { - log.Error("configuration error: config file should have only two top level elements: `vouch` and `oauth`. These and other syntax errors follow...") - log.Error(err) - log.Error("continuing... (maybe you know what you're doing :)") - } - if err = UnmarshalKey(Branding.LCName, &Cfg); err != nil { log.Error(err) } - if len(Cfg.Domains) == 0 { // then lets check for "lasso" var oldConfig = &Config{} @@ -325,23 +303,6 @@ please update your config file to change '%s:' to '%s:' as per %s // log.Debugf("secret: %s", string(Cfg.JWT.Secret)) } -// use viper and mapstructure check to see if -// https://pkg.go.dev/github.com/spf13/viper@v1.6.3?tab=doc#Unmarshal -// https://pkg.go.dev/github.com/mitchellh/mapstructure?tab=doc#DecoderConfig -func checkConfigFileWellFormed() error { - opt := func(dc *mapstructure.DecoderConfig) { - dc.ErrorUnused = true - } - - type quick struct { - Vouch Config - OAuth oauthConfig - } - q := &quick{} - - return viper.Unmarshal(q, opt) -} - // UnmarshalKey populate struct from contents of cfg tree at key func UnmarshalKey(key string, rawVal interface{}) error { return viper.UnmarshalKey(key, rawVal) @@ -411,6 +372,24 @@ func basicTest() error { return nil } +func checkCallbackConfig(url string) error { + inDomain := false + for _, d := range Cfg.Domains { + if strings.Contains(url, d) { + inDomain = true + break + } + } + if !inDomain { + return fmt.Errorf("configuration error: oauth.callback_url (%s) must be within the configured domain where the cookie will be set %s", url, Cfg.Domains) + } + + if !strings.Contains(url, "/auth") { + return fmt.Errorf("configuration error: oauth.callback_url (%s) must contain '/auth'", url) + } + return nil +} + // setDefaults set default options for most items func setDefaults() { @@ -535,10 +514,9 @@ func claimToHeader(claim string) (string, error) { // not allowed in header: "(),/:;<=>?@[\]{}" // https://greenbytes.de/tech/webdav/rfc7230.html#rfc.section.3.2.6 - // and we don't allow underscores `_` or periods `.` because nginx doesn't like them - // "Valid names are composed of English letters, digits, hyphens, and possibly underscores" - // as per http://nginx.org/en/docs/http/ngx_http_core_module.html#underscores_in_headers - for _, r := range `"(),/\:;<=>?@[]{}_.` { + // and we don't allow underscores because nginx doesn't like them + // http://nginx.org/en/docs/http/ngx_http_core_module.html#underscores_in_headers + for _, r := range `"(),/\:;<=>?@[]{}_` { claim = strings.ReplaceAll(claim, string(r), "-") } diff --git a/pkg/cfg/cfg_test.go b/pkg/cfg/cfg_test.go index 571844aa..4cc3b497 100644 --- a/pkg/cfg/cfg_test.go +++ b/pkg/cfg/cfg_test.go @@ -1,13 +1,3 @@ -/* - -Copyright 2020 The Vouch Proxy Authors. -Use of this source code is governed by The MIT License (MIT) that -can be found in the LICENSE file. Software distributed under The -MIT License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES -OR CONDITIONS OF ANY KIND, either express or implied. - -*/ - package cfg import ( @@ -52,9 +42,9 @@ func Test_claimToHeader(t *testing.T) { want string wantErr bool }{ - {"remove http://", "http://test.example.com", Cfg.Headers.ClaimHeader + "Test-Example-Com", false}, - {"remove https://", "https://test.example.com", Cfg.Headers.ClaimHeader + "Test-Example-Com", false}, - {"auth0 fix https://", "https://test.auth0.com/user", Cfg.Headers.ClaimHeader + "Test-Auth0-Com-User", false}, + {"remove http://", "http://test.example.com", Cfg.Headers.ClaimHeader + "Test.example.com", false}, + {"remove https://", "https://test.example.com", Cfg.Headers.ClaimHeader + "Test.example.com", false}, + {"auth0 fix https://", "https://test.auth0.com/user", Cfg.Headers.ClaimHeader + "Test.auth0.com-User", false}, {"cognito user:groups", "user:groups", Cfg.Headers.ClaimHeader + "User-Groups", false}, } for _, tt := range tests { diff --git a/pkg/cfg/jwt.go b/pkg/cfg/jwt.go index 77e5c168..376fedf6 100644 --- a/pkg/cfg/jwt.go +++ b/pkg/cfg/jwt.go @@ -1,13 +1,3 @@ -/* - -Copyright 2020 The Vouch Proxy Authors. -Use of this source code is governed by The MIT License (MIT) that -can be found in the LICENSE file. Software distributed under The -MIT License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES -OR CONDITIONS OF ANY KIND, either express or implied. - -*/ - package cfg import ( diff --git a/pkg/cfg/logging.go b/pkg/cfg/logging.go index 9c368e40..a64b7d3a 100644 --- a/pkg/cfg/logging.go +++ b/pkg/cfg/logging.go @@ -1,13 +1,3 @@ -/* - -Copyright 2020 The Vouch Proxy Authors. -Use of this source code is governed by The MIT License (MIT) that -can be found in the LICENSE file. Software distributed under The -MIT License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES -OR CONDITIONS OF ANY KIND, either express or implied. - -*/ - package cfg import ( @@ -25,6 +15,7 @@ type logging struct { FastLogger *zap.Logger AtomicLogLevel zap.AtomicLevel DefaultLogLevel zapcore.Level + LogLevel zapcore.Level } var ( @@ -64,7 +55,8 @@ func init() { func (logging) setLogLevel(lvl zapcore.Level) { // https://github.com/uber-go/zap/blob/master/zapcore/level.go#L59 - if Logging.AtomicLogLevel.Level() != lvl { + if Logging.LogLevel != lvl { + Logging.LogLevel = lvl log.Infof("setting LogLevel to %s", lvl) Logging.AtomicLogLevel.SetLevel(lvl) } @@ -109,7 +101,7 @@ func (logging) configure() { Cfg.LogLevel = fmt.Sprintf("%s", Logging.DefaultLogLevel) } - if Cfg.LogLevel != Logging.AtomicLogLevel.Level().String() { + if Cfg.LogLevel != Logging.LogLevel.String() { // log.Errorf("Logging.configure() Logging.LogLevel %s Cfg.LogLevel %s", Logging.LogLeveLogging.String(), Cfg.LogLevel) Logging.setLogLevelString(Cfg.LogLevel) } @@ -126,12 +118,7 @@ func (logging) configureFromCmdline() { if *CmdLine.logLevel != cmdLineLoggingDefault { Logging.setLogLevel(*CmdLine.logLevel) // defaults to Logging.DefaultLogLevel which is zap.InfoLevel - log.Info("logging configured from cmdline") - // if we're supposed to run tests, run tests and exit - if *CmdLine.logTest { - Logging.cmdlineTestLogs() - } - + log.Error("logging configured from cmdline") configured = true } } diff --git a/pkg/cfg/logging_test.go b/pkg/cfg/logging_test.go index 8b2d2a52..f83b3f7d 100644 --- a/pkg/cfg/logging_test.go +++ b/pkg/cfg/logging_test.go @@ -1,13 +1,3 @@ -/* - -Copyright 2020 The Vouch Proxy Authors. -Use of this source code is governed by The MIT License (MIT) that -can be found in the LICENSE file. Software distributed under The -MIT License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES -OR CONDITIONS OF ANY KIND, either express or implied. - -*/ - package cfg import ( diff --git a/pkg/cfg/oauth.go b/pkg/cfg/oauth.go index 95a5b7b2..42421352 100644 --- a/pkg/cfg/oauth.go +++ b/pkg/cfg/oauth.go @@ -1,20 +1,9 @@ -/* - -Copyright 2020 The Vouch Proxy Authors. -Use of this source code is governed by The MIT License (MIT) that -can be found in the LICENSE file. Software distributed under The -MIT License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES -OR CONDITIONS OF ANY KIND, either express or implied. - -*/ - package cfg import ( "errors" - "fmt" - "strings" + "github.com/spf13/viper" "golang.org/x/oauth2" "golang.org/x/oauth2/github" "golang.org/x/oauth2/google" @@ -48,17 +37,19 @@ func oauthBasicTest() error { return errors.New("configuration error: oauth.user_info_url not found") } - if GenOAuth.RedirectURL != "" { - if err := checkCallbackConfig(GenOAuth.RedirectURL); err != nil { - return err - } - } - if len(GenOAuth.RedirectURLs) > 0 { - for _, cb := range GenOAuth.RedirectURLs { - if err := checkCallbackConfig(cb); err != nil { + if !viper.IsSet(Branding.LCName + ".allowAllUsers") { + if GenOAuth.RedirectURL != "" { + if err := checkCallbackConfig(GenOAuth.RedirectURL); err != nil { return err } } + if len(GenOAuth.RedirectURLs) > 0 { + for _, cb := range GenOAuth.RedirectURLs { + if err := checkCallbackConfig(cb); err != nil { + return err + } + } + } } return nil } @@ -145,22 +136,3 @@ func configureOAuthClient() { Scopes: GenOAuth.Scopes, } } - -func checkCallbackConfig(url string) error { - if !strings.Contains(url, "/auth") { - log.Errorf("configuration error: oauth.callback_url (%s) should almost always point at the vouch-proxy '/auth' endpoint", url) - } - - found := false - for _, d := range append(Cfg.Domains, Cfg.Cookie.Domain) { - if d != "" && strings.Contains(url, d) { - found = true - break - } - } - if !found { - return fmt.Errorf("configuration error: oauth.callback_url (%s) must be within a configured domains where the cookie will be set: either `vouch.domains` %s or `vouch.cookie.domain` %s", url, Cfg.Domains, Cfg.Cookie.Domain) - } - - return nil -} diff --git a/pkg/cfg/oauth_test.go b/pkg/cfg/oauth_test.go deleted file mode 100644 index f86b07ec..00000000 --- a/pkg/cfg/oauth_test.go +++ /dev/null @@ -1,42 +0,0 @@ -/* - -Copyright 2020 The Vouch Proxy Authors. -Use of this source code is governed by The MIT License (MIT) that -can be found in the LICENSE file. Software distributed under The -MIT License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES -OR CONDITIONS OF ANY KIND, either express or implied. - -*/ - -package cfg - -import ( - "os" - "path/filepath" - "testing" -) - -func setUp(configFile string) { - os.Setenv("VOUCH_CONFIG", filepath.Join(os.Getenv("VOUCH_ROOT"), configFile)) - InitForTestPurposes() -} - -func Test_checkCallbackConfig(t *testing.T) { - setUp("/config/testing/handler_login_url.yml") - - tests := []struct { - name string - url string - wantErr bool - }{ - {"correct", "http://vouch.example.com:9090/auth", false}, - {"bad", "http://vouch.notgonna.com:9090/somewhereelse", true}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := checkCallbackConfig(tt.url); (err != nil) != tt.wantErr { - t.Errorf("checkCallbackConfig() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} diff --git a/pkg/cookie/cookie.go b/pkg/cookie/cookie.go index 1db6553d..e835be95 100644 --- a/pkg/cookie/cookie.go +++ b/pkg/cookie/cookie.go @@ -1,13 +1,3 @@ -/* - -Copyright 2020 The Vouch Proxy Authors. -Use of this source code is governed by The MIT License (MIT) that -can be found in the LICENSE file. Software distributed under The -MIT License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES -OR CONDITIONS OF ANY KIND, either express or implied. - -*/ - package cookie import ( @@ -47,7 +37,21 @@ func setCookie(w http.ResponseWriter, r *http.Request, val string, maxAge int) { domain = cfg.Cfg.Cookie.Domain log.Debugf("setting the cookie domain to %v", domain) } - sameSite := SameSite() + + sameSite := http.SameSite(0) + if cfg.Cfg.Cookie.SameSite != "" { + switch strings.ToLower(cfg.Cfg.Cookie.SameSite) { + case "lax": + sameSite = http.SameSiteLaxMode + case "strict": + sameSite = http.SameSiteStrictMode + case "none": + if cfg.Cfg.Cookie.Secure == false { + log.Error("SameSite cookie attribute with sameSite=none should also be specified with secure=true.") + } + sameSite = http.SameSiteNoneMode + } + } cookie := http.Cookie{ Name: cfg.Cfg.Cookie.Name, @@ -173,27 +177,6 @@ func ClearCookie(w http.ResponseWriter, r *http.Request) { } } -// SameSite return cfg.Cfg.Cookie.SameSite as http.Samesite -// if cfg.Cfg.Cookie.SameSite is unconfigured return http.SameSite(0) -// see https://github.com/vouch/vouch-proxy/issues/210 -func SameSite() http.SameSite { - sameSite := http.SameSite(0) - if cfg.Cfg.Cookie.SameSite != "" { - switch strings.ToLower(cfg.Cfg.Cookie.SameSite) { - case "lax": - sameSite = http.SameSiteLaxMode - case "strict": - sameSite = http.SameSiteStrictMode - case "none": - if cfg.Cfg.Cookie.Secure == false { - log.Error("SameSite cookie attribute with sameSite=none should also be specified with secure=true.") - } - sameSite = http.SameSiteNoneMode - } - } - return sameSite -} - // splitCookie separate string into several strings of specified length func splitCookie(longString string, maxLen int) []string { splits := make([]string, 0) diff --git a/pkg/cookie/cookie_test.go b/pkg/cookie/cookie_test.go index 93222c02..041afcb9 100644 --- a/pkg/cookie/cookie_test.go +++ b/pkg/cookie/cookie_test.go @@ -1,13 +1,3 @@ -/* - -Copyright 2020 The Vouch Proxy Authors. -Use of this source code is governed by The MIT License (MIT) that -can be found in the LICENSE file. Software distributed under The -MIT License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES -OR CONDITIONS OF ANY KIND, either express or implied. - -*/ - package cookie import ( diff --git a/pkg/domains/domains.go b/pkg/domains/domains.go index ae215908..4845d8a2 100644 --- a/pkg/domains/domains.go +++ b/pkg/domains/domains.go @@ -1,13 +1,3 @@ -/* - -Copyright 2020 The Vouch Proxy Authors. -Use of this source code is governed by The MIT License (MIT) that -can be found in the LICENSE file. Software distributed under The -MIT License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES -OR CONDITIONS OF ANY KIND, either express or implied. - -*/ - package domains import ( diff --git a/pkg/domains/domains_test.go b/pkg/domains/domains_test.go index deb57ffe..f36923b5 100644 --- a/pkg/domains/domains_test.go +++ b/pkg/domains/domains_test.go @@ -1,13 +1,3 @@ -/* - -Copyright 2020 The Vouch Proxy Authors. -Use of this source code is governed by The MIT License (MIT) that -can be found in the LICENSE file. Software distributed under The -MIT License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES -OR CONDITIONS OF ANY KIND, either express or implied. - -*/ - package domains import ( diff --git a/pkg/healthcheck/healthcheck.go b/pkg/healthcheck/healthcheck.go index f602edbd..cd45bdbd 100644 --- a/pkg/healthcheck/healthcheck.go +++ b/pkg/healthcheck/healthcheck.go @@ -1,13 +1,3 @@ -/* - -Copyright 2020 The Vouch Proxy Authors. -Use of this source code is governed by The MIT License (MIT) that -can be found in the LICENSE file. Software distributed under The -MIT License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES -OR CONDITIONS OF ANY KIND, either express or implied. - -*/ - package healthcheck import ( diff --git a/pkg/jwtmanager/jwtmanager.go b/pkg/jwtmanager/jwtmanager.go index 13943469..0a549a35 100644 --- a/pkg/jwtmanager/jwtmanager.go +++ b/pkg/jwtmanager/jwtmanager.go @@ -1,13 +1,3 @@ -/* - -Copyright 2020 The Vouch Proxy Authors. -Use of this source code is governed by The MIT License (MIT) that -can be found in the LICENSE file. Software distributed under The -MIT License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES -OR CONDITIONS OF ANY KIND, either express or implied. - -*/ - package jwtmanager import ( diff --git a/pkg/jwtmanager/jwtmanager_test.go b/pkg/jwtmanager/jwtmanager_test.go index eb844ff3..7b6fd5d2 100644 --- a/pkg/jwtmanager/jwtmanager_test.go +++ b/pkg/jwtmanager/jwtmanager_test.go @@ -1,13 +1,3 @@ -/* - -Copyright 2020 The Vouch Proxy Authors. -Use of this source code is governed by The MIT License (MIT) that -can be found in the LICENSE file. Software distributed under The -MIT License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES -OR CONDITIONS OF ANY KIND, either express or implied. - -*/ - package jwtmanager import ( diff --git a/pkg/response/response.go b/pkg/response/response.go index 4c0511db..893eadb3 100644 --- a/pkg/response/response.go +++ b/pkg/response/response.go @@ -1,13 +1,3 @@ -/* - -Copyright 2020 The Vouch Proxy Authors. -Use of this source code is governed by The MIT License (MIT) that -can be found in the LICENSE file. Software distributed under The -MIT License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES -OR CONDITIONS OF ANY KIND, either express or implied. - -*/ - package response import ( diff --git a/pkg/structs/structs.go b/pkg/structs/structs.go index cec3a700..93a70acd 100644 --- a/pkg/structs/structs.go +++ b/pkg/structs/structs.go @@ -1,13 +1,3 @@ -/* - -Copyright 2020 The Vouch Proxy Authors. -Use of this source code is governed by The MIT License (MIT) that -can be found in the LICENSE file. Software distributed under The -MIT License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES -OR CONDITIONS OF ANY KIND, either express or implied. - -*/ - package structs // CustomClaims Temporary struct storing custom claims until JWT creation. diff --git a/pkg/timelog/timelog.go b/pkg/timelog/timelog.go index b2ea24c2..92bb41de 100644 --- a/pkg/timelog/timelog.go +++ b/pkg/timelog/timelog.go @@ -1,13 +1,3 @@ -/* - -Copyright 2020 The Vouch Proxy Authors. -Use of this source code is governed by The MIT License (MIT) that -can be found in the LICENSE file. Software distributed under The -MIT License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES -OR CONDITIONS OF ANY KIND, either express or implied. - -*/ - package timelog import (