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 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 diff --git a/handlers/handlers.go b/handlers/handlers.go index f826fce6..ffc26d9c 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" @@ -48,7 +50,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 { @@ -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,72 @@ 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") + 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") + + client := &http.Client{} + userinfo, err := client.Do(req) + + if err != nil { + return err + } + defer userinfo.Body.Close() + + body, _ := ioutil.ReadAll(userinfo.Body) + tokenRes := adfsTokenRes{} + + 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.Username + 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 251ec7f0..da38f5d1 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", } @@ -236,8 +238,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") } @@ -404,6 +406,9 @@ func setDefaults() { } else if GenOAuth.Provider == Providers.GitHub { setDefaultsGitHub() configureOAuthClient() + } else if GenOAuth.Provider == Providers.ADFS { + setDefaultsADFS() + configureOAuthClient() } else { configureOAuthClient() } @@ -429,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/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