From c3ea803e765a70509b609141233b772b828a1f16 Mon Sep 17 00:00:00 2001 From: Simon Gottschlag Date: Tue, 5 Feb 2019 16:52:40 +0100 Subject: [PATCH 1/8] Fix cookies (#3) fix cookies --- pkg/cfg/cfg.go | 10 ++++++++++ pkg/cookie/cookie.go | 17 +++++++++-------- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/pkg/cfg/cfg.go b/pkg/cfg/cfg.go index 251ec7f0..b8177053 100644 --- a/pkg/cfg/cfg.go +++ b/pkg/cfg/cfg.go @@ -213,6 +213,16 @@ func Get(key string) string { return viper.GetString(key) } +// Get int value for key +func GetInt(key string) int { + return viper.GetInt(key) +} + +// Get bool value for key +func GetBool(key string) bool { + return viper.GetBool(key) +} + // BasicTest just a quick sanity check to see if the config is sound func BasicTest() error { for _, opt := range RequiredOptions { diff --git a/pkg/cookie/cookie.go b/pkg/cookie/cookie.go index f6a140b3..2a48a5ca 100644 --- a/pkg/cookie/cookie.go +++ b/pkg/cookie/cookie.go @@ -10,7 +10,8 @@ import ( "github.com/vouch/vouch-proxy/pkg/domains" ) -var defaultMaxAge = cfg.Cfg.JWT.MaxAge * 60 +var defaultMaxAge = cfg.GetInt("JWT.MaxAge") * 60 + // SetCookie http func SetCookie(w http.ResponseWriter, r *http.Request, val string) { @@ -24,25 +25,25 @@ 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 + if cfg.Get("Cookie.Domain") != "" { + domain = cfg.Get("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, + Name: cfg.Get("Cookie.Name"), Value: val, Path: "/", Domain: domain, MaxAge: maxAge, - Secure: cfg.Cfg.Cookie.Secure, - HttpOnly: cfg.Cfg.Cookie.HTTPOnly, + Secure: cfg.GetBool("Cookie.Secure"), + HttpOnly: cfg.GetBool("Cookie.HTTPOnly"), }) } // Cookie get the vouch jwt cookie func Cookie(r *http.Request) (string, error) { - cookie, err := r.Cookie(cfg.Cfg.Cookie.Name) + cookie, err := r.Cookie(cfg.Get("Cookie.Name")) if err != nil { return "", err } @@ -51,7 +52,7 @@ func Cookie(r *http.Request) (string, error) { } log.WithFields(log.Fields{ - "cookieName": cfg.Cfg.Cookie.Name, + "cookieName": cfg.Get("Cookie.Name"), "cookieValue": cookie.Value, }).Debug("cookie") return cookie.Value, err From 957647bb1b9e457ccf0dbe90c16ab4d727b5fc58 Mon Sep 17 00:00:00 2001 From: Simon Gottschlag Date: Tue, 5 Feb 2019 16:53:11 +0100 Subject: [PATCH 2/8] change encoding to support adfs (#4) --- handlers/handlers.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/handlers/handlers.go b/handlers/handlers.go index f826fce6..475a035f 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -48,7 +48,7 @@ var ( func randString() string { b := make([]byte, 32) rand.Read(b) - return base64.StdEncoding.EncodeToString(b) + return base64.URLEncoding.EncodeToString(b) } func loginURL(r *http.Request, state string) string { From 6e00591333d7d5a9c23a0ace2edb8408032891da Mon Sep 17 00:00:00 2001 From: Simon Gottschlag Date: Wed, 6 Feb 2019 13:41:17 +0100 Subject: [PATCH 3/8] Add initial support for ADFS --- handlers/handlers.go | 73 ++++++++++++++++++++++++++++++++++++++++++ pkg/cfg/cfg.go | 10 +++--- pkg/structs/structs.go | 16 +++++++++ 3 files changed, 95 insertions(+), 4 deletions(-) diff --git a/handlers/handlers.go b/handlers/handlers.go index 475a035f..7ccc3a3e 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -10,6 +10,8 @@ import ( "io/ioutil" "mime/multipart" "net/http" + "net/url" + "strconv" "strings" log "github.com/Sirupsen/logrus" @@ -424,6 +426,8 @@ 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 == cfg.Providers.IndieAuth { return getUserInfoFromIndieAuth(r, user) + } else if cfg.GenOAuth.Provider == cfg.Providers.ADFS { + return getUserInfoFromADFS(r, user) } providerToken, err := cfg.OAuthClient.Exchange(oauth2.NoContext, r.URL.Query().Get("code")) @@ -569,6 +573,75 @@ func getUserInfoFromIndieAuth(r *http.Request, user *structs.User) error { return nil } +// More info: https://docs.microsoft.com/en-us/windows-server/identity/ad-fs/overview/ad-fs-scenarios-for-developers#supported-scenarios +func getUserInfoFromADFS(r *http.Request, user *structs.User) error { + code := r.URL.Query().Get("code") + log.Errorf("code: %s", code) + + formData := url.Values{} + formData.Set("code", code) + formData.Set("grant_type", "authorization_code") + formData.Set("resource", cfg.GenOAuth.RedirectURL) + formData.Set("client_id", cfg.GenOAuth.ClientID) + formData.Set("redirect_uri", cfg.GenOAuth.RedirectURL) + formData.Set("client_secret", cfg.GenOAuth.ClientSecret) + + req, err := http.NewRequest("POST", cfg.GenOAuth.TokenURL, strings.NewReader(formData.Encode())) + if err != nil { + return err + } + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + req.Header.Add("Content-Length", strconv.Itoa(len(formData.Encode()))) + req.Header.Set("Accept", "application/json") + + // v := url.Values{} + // userinfo, err := client.PostForm(cfg.GenOAuth.UserInfoURL, v) + + client := &http.Client{} + userinfo, err := client.Do(req) + + if err != nil { + // http.Error(w, err.Error(), http.StatusBadRequest) + return err + } + defer userinfo.Body.Close() + + body, _ := ioutil.ReadAll(userinfo.Body) + + var tokenRes struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + IDToken string `json:"id_token"` + ExpiresIn int64 `json:"expires_in"` // relative seconds from now + } + + if err := json.Unmarshal(body, &tokenRes); err != nil { + log.Errorf("oauth2: cannot fetch token: %v", err) + return nil + } + + s := strings.Split(tokenRes.IDToken, ".") + if len(s) < 2 { + log.Error("jws: invalid token received") + return nil + } + + idToken, err := base64.RawURLEncoding.DecodeString(s[1]) + if err != nil { + log.Error(err) + return nil + } + + adfsUser := structs.ADFSUser{} + json.Unmarshal([]byte(idToken), &adfsUser) + log.Println("adfs adfsUser: ", adfsUser) + + adfsUser.PrepareUserData() + user.Username = adfsUser.UPN + log.Debug(user) + 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) { diff --git a/pkg/cfg/cfg.go b/pkg/cfg/cfg.go index b8177053..cbb76b80 100644 --- a/pkg/cfg/cfg.go +++ b/pkg/cfg/cfg.go @@ -79,6 +79,7 @@ type OAuthProviders struct { Google string GitHub string IndieAuth string + ADFS string OIDC string } @@ -113,6 +114,7 @@ var ( Google: "google", GitHub: "github", IndieAuth: "indieauth", + ADFS: "adfs", OIDC: "oidc", } @@ -213,12 +215,12 @@ func Get(key string) string { return viper.GetString(key) } -// Get int value for key +// GetInt int value for key func GetInt(key string) int { return viper.GetInt(key) } -// Get bool value for key +// GetBool bool value for key func GetBool(key string) bool { return viper.GetBool(key) } @@ -246,8 +248,8 @@ func BasicTest() error { case GenOAuth.Provider != Providers.Google && GenOAuth.AuthURL == "": // everyone except IndieAuth and Google has an authURL return errors.New("configuration error: oauth.auth_url not found") - case GenOAuth.Provider != Providers.Google && GenOAuth.Provider != Providers.IndieAuth && GenOAuth.UserInfoURL == "": - // everyone except IndieAuth and Google has an userInfoURL + case GenOAuth.Provider != Providers.Google && GenOAuth.Provider != Providers.IndieAuth && GenOAuth.Provider != Providers.ADFS && GenOAuth.UserInfoURL == "": + // everyone except IndieAuth, Google and ADFS has an userInfoURL return errors.New("configuration error: oauth.user_info_url not found") } diff --git a/pkg/structs/structs.go b/pkg/structs/structs.go index 62ab2481..af99ff65 100644 --- a/pkg/structs/structs.go +++ b/pkg/structs/structs.go @@ -47,6 +47,22 @@ func (u *GoogleUser) PrepareUserData() { u.Username = u.Email } +type ADFSUser struct { + User + Sub string `json:"sub"` + UPN string `json:"upn"` + // UniqueName string `json:"unique_name"` + // PwdExp string `json:"pwd_exp"` + // SID string `json:"sid"` + // Groups string `json:"groups"` + // jwt.StandardClaims +} + +// PrepareUserData implement PersonalData interface +func (u *ADFSUser) PrepareUserData() { + u.Username = u.UPN +} + // GitHubUser is a retrieved and authentiacted user from GitHub. type GitHubUser struct { User From 40d54519fdb4f216123f186b11944da2dc4a7244 Mon Sep 17 00:00:00 2001 From: Benjamin Foote Date: Thu, 7 Feb 2019 17:33:14 -0800 Subject: [PATCH 4/8] ignore local configs --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 3c1e74bf..348f0c94 100644 --- a/.gitignore +++ b/.gitignore @@ -8,8 +8,10 @@ main config/google_config.json .vscode/* vouch -config/config.yml_google +config/config.yml_adfs config/config.yml_github +config/config.yml_google +config/config.yml_oidc config/secret config/config.yml_orig data/vouch_bolt.db From e09ad22bb99713a179c3fa9cf447fd127988f498 Mon Sep 17 00:00:00 2001 From: Benjamin Foote Date: Thu, 7 Feb 2019 19:46:05 -0800 Subject: [PATCH 5/8] #65 check your indentation (yaml is so spacey) --- config/config.yml_example | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/config.yml_example b/config/config.yml_example index 37e211c6..dbc8a3a7 100644 --- a/config/config.yml_example +++ b/config/config.yml_example @@ -3,6 +3,8 @@ # you should probably start with one of the other configs in the example directory # vouch proxy does a fairly good job of setting its config to sane defaults +# be aware of your indentation, the only top level elements are `vouch` and `oauth`. + vouch: # logLevel: debug logLevel: info From 8ec8485c43d922977f706c148bc99ebc798d67e8 Mon Sep 17 00:00:00 2001 From: Benjamin Foote Date: Thu, 7 Feb 2019 19:58:35 -0800 Subject: [PATCH 6/8] Revert "Fix cookies (#3)" This reverts commit c3ea803e765a70509b609141233b772b828a1f16. --- pkg/cfg/cfg.go | 10 ---------- pkg/cookie/cookie.go | 17 ++++++++--------- 2 files changed, 8 insertions(+), 19 deletions(-) diff --git a/pkg/cfg/cfg.go b/pkg/cfg/cfg.go index cbb76b80..0bdd466d 100644 --- a/pkg/cfg/cfg.go +++ b/pkg/cfg/cfg.go @@ -215,16 +215,6 @@ func Get(key string) string { return viper.GetString(key) } -// GetInt int value for key -func GetInt(key string) int { - return viper.GetInt(key) -} - -// GetBool bool value for key -func GetBool(key string) bool { - return viper.GetBool(key) -} - // BasicTest just a quick sanity check to see if the config is sound func BasicTest() error { for _, opt := range RequiredOptions { diff --git a/pkg/cookie/cookie.go b/pkg/cookie/cookie.go index 2a48a5ca..f6a140b3 100644 --- a/pkg/cookie/cookie.go +++ b/pkg/cookie/cookie.go @@ -10,8 +10,7 @@ import ( "github.com/vouch/vouch-proxy/pkg/domains" ) -var defaultMaxAge = cfg.GetInt("JWT.MaxAge") * 60 - +var defaultMaxAge = cfg.Cfg.JWT.MaxAge * 60 // SetCookie http func SetCookie(w http.ResponseWriter, r *http.Request, val string) { @@ -25,25 +24,25 @@ 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.Get("Cookie.Domain") != "" { - domain = cfg.Get("Cookie.Domain") + 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.Get("Cookie.Name"), + Name: cfg.Cfg.Cookie.Name, Value: val, Path: "/", Domain: domain, MaxAge: maxAge, - Secure: cfg.GetBool("Cookie.Secure"), - HttpOnly: cfg.GetBool("Cookie.HTTPOnly"), + Secure: cfg.Cfg.Cookie.Secure, + HttpOnly: cfg.Cfg.Cookie.HTTPOnly, }) } // Cookie get the vouch jwt cookie func Cookie(r *http.Request) (string, error) { - cookie, err := r.Cookie(cfg.Get("Cookie.Name")) + cookie, err := r.Cookie(cfg.Cfg.Cookie.Name) if err != nil { return "", err } @@ -52,7 +51,7 @@ func Cookie(r *http.Request) (string, error) { } log.WithFields(log.Fields{ - "cookieName": cfg.Get("Cookie.Name"), + "cookieName": cfg.Cfg.Cookie.Name, "cookieValue": cookie.Value, }).Debug("cookie") return cookie.Value, err From 13283386574ec3d87afc8d0a1a2d6a8779ed0f11 Mon Sep 17 00:00:00 2001 From: Benjamin Foote Date: Thu, 7 Feb 2019 20:26:48 -0800 Subject: [PATCH 7/8] #68 move struct outside of function, minor cleanup --- handlers/handlers.go | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/handlers/handlers.go b/handlers/handlers.go index 7ccc3a3e..ffc26d9c 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -573,6 +573,13 @@ func getUserInfoFromIndieAuth(r *http.Request, user *structs.User) error { return nil } +type adfsTokenRes struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + IDToken string `json:"id_token"` + ExpiresIn int64 `json:"expires_in"` // relative seconds from now +} + // More info: https://docs.microsoft.com/en-us/windows-server/identity/ad-fs/overview/ad-fs-scenarios-for-developers#supported-scenarios func getUserInfoFromADFS(r *http.Request, user *structs.User) error { code := r.URL.Query().Get("code") @@ -594,26 +601,16 @@ func getUserInfoFromADFS(r *http.Request, user *structs.User) error { req.Header.Add("Content-Length", strconv.Itoa(len(formData.Encode()))) req.Header.Set("Accept", "application/json") - // v := url.Values{} - // userinfo, err := client.PostForm(cfg.GenOAuth.UserInfoURL, v) - client := &http.Client{} userinfo, err := client.Do(req) if err != nil { - // http.Error(w, err.Error(), http.StatusBadRequest) return err } defer userinfo.Body.Close() body, _ := ioutil.ReadAll(userinfo.Body) - - var tokenRes struct { - AccessToken string `json:"access_token"` - TokenType string `json:"token_type"` - IDToken string `json:"id_token"` - ExpiresIn int64 `json:"expires_in"` // relative seconds from now - } + tokenRes := adfsTokenRes{} if err := json.Unmarshal(body, &tokenRes); err != nil { log.Errorf("oauth2: cannot fetch token: %v", err) @@ -637,7 +634,7 @@ func getUserInfoFromADFS(r *http.Request, user *structs.User) error { log.Println("adfs adfsUser: ", adfsUser) adfsUser.PrepareUserData() - user.Username = adfsUser.UPN + user.Username = adfsUser.Username log.Debug(user) return nil } From ed8ef3b963d559d8e520c8b7cf4fa13db91fa802 Mon Sep 17 00:00:00 2001 From: Simon Gottschlag Date: Fri, 8 Feb 2019 06:49:14 +0100 Subject: [PATCH 8/8] Add resource to redirect query --- pkg/cfg/cfg.go | 18 ++++++++---------- pkg/cookie/cookie.go | 17 ++++++++--------- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/pkg/cfg/cfg.go b/pkg/cfg/cfg.go index cbb76b80..da38f5d1 100644 --- a/pkg/cfg/cfg.go +++ b/pkg/cfg/cfg.go @@ -215,16 +215,6 @@ func Get(key string) string { return viper.GetString(key) } -// GetInt int value for key -func GetInt(key string) int { - return viper.GetInt(key) -} - -// GetBool bool value for key -func GetBool(key string) bool { - return viper.GetBool(key) -} - // BasicTest just a quick sanity check to see if the config is sound func BasicTest() error { for _, opt := range RequiredOptions { @@ -416,6 +406,9 @@ func setDefaults() { } else if GenOAuth.Provider == Providers.GitHub { setDefaultsGitHub() configureOAuthClient() + } else if GenOAuth.Provider == Providers.ADFS { + setDefaultsADFS() + configureOAuthClient() } else { configureOAuthClient() } @@ -441,6 +434,11 @@ func setDefaultsGoogle() { } } +func setDefaultsADFS() { + log.Info("configuring ADFS OAuth") + OAuthopts = oauth2.SetAuthURLParam("resource", GenOAuth.RedirectURL) // Needed or all claims won't be included +} + func setDefaultsGitHub() { // log.Info("configuring GitHub OAuth") if GenOAuth.AuthURL == "" { diff --git a/pkg/cookie/cookie.go b/pkg/cookie/cookie.go index 2a48a5ca..f6a140b3 100644 --- a/pkg/cookie/cookie.go +++ b/pkg/cookie/cookie.go @@ -10,8 +10,7 @@ import ( "github.com/vouch/vouch-proxy/pkg/domains" ) -var defaultMaxAge = cfg.GetInt("JWT.MaxAge") * 60 - +var defaultMaxAge = cfg.Cfg.JWT.MaxAge * 60 // SetCookie http func SetCookie(w http.ResponseWriter, r *http.Request, val string) { @@ -25,25 +24,25 @@ 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.Get("Cookie.Domain") != "" { - domain = cfg.Get("Cookie.Domain") + 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.Get("Cookie.Name"), + Name: cfg.Cfg.Cookie.Name, Value: val, Path: "/", Domain: domain, MaxAge: maxAge, - Secure: cfg.GetBool("Cookie.Secure"), - HttpOnly: cfg.GetBool("Cookie.HTTPOnly"), + Secure: cfg.Cfg.Cookie.Secure, + HttpOnly: cfg.Cfg.Cookie.HTTPOnly, }) } // Cookie get the vouch jwt cookie func Cookie(r *http.Request) (string, error) { - cookie, err := r.Cookie(cfg.Get("Cookie.Name")) + cookie, err := r.Cookie(cfg.Cfg.Cookie.Name) if err != nil { return "", err } @@ -52,7 +51,7 @@ func Cookie(r *http.Request) (string, error) { } log.WithFields(log.Fields{ - "cookieName": cfg.Get("Cookie.Name"), + "cookieName": cfg.Cfg.Cookie.Name, "cookieValue": cookie.Value, }).Debug("cookie") return cookie.Value, err