diff --git a/config/config.yml_example b/config/config.yml_example index 37e211c6..cc4d593e 100644 --- a/config/config.yml_example +++ b/config/config.yml_example @@ -73,6 +73,14 @@ vouch: jwt: X-Vouch-Token querystring: access_token redirect: X-Vouch-Requested-URI + # If idToken is defined, all valid requests to /validate will receive a header (defined in the value) containing the id_token from the OpenID Provider + # If idToken is empty or undefined, no header will be added + # This will make the response largerwich in turn can affect the response time + # idToken: x-vouch-idtoken + # If accessToken is defined, all valid requests to /validate will receive a header (defined in the value) containing the access_token from the OpenID Provider + # If accessToken is empty or undefined, no header will be added + # This will make the response larger wich in turn can affect the response time + # accessToken: x-vouch-accesstoken db: file: data/vouch_bolt.db diff --git a/handlers/handlers.go b/handlers/handlers.go index f826fce6..e3bfb870 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 { @@ -73,6 +75,8 @@ func loginURL(r *http.Request, state string) string { } } else if cfg.GenOAuth.Provider == cfg.Providers.IndieAuth { url = cfg.OAuthClient.AuthCodeURL(state, oauth2.SetAuthURLParam("response_type", "id")) + } else if cfg.GenOAuth.Provider == cfg.Providers.ADFS { + url = cfg.OAuthClient.AuthCodeURL(state, cfg.OAuthopts) } else { url = cfg.OAuthClient.AuthCodeURL(state) } @@ -188,6 +192,12 @@ func ValidateRequestHandler(w http.ResponseWriter, r *http.Request) { } w.Header().Add(cfg.Cfg.Headers.User, claims.Username) + if cfg.Cfg.Headers.IDToken != "" { + w.Header().Add(cfg.Cfg.Headers.IDToken, claims.IDToken) + } + if cfg.Cfg.Headers.AccessToken != "" { + w.Header().Add(cfg.Cfg.Headers.AccessToken, claims.AccessToken) + } w.Header().Add(cfg.Cfg.Headers.Success, "true") log.WithFields(log.Fields{cfg.Cfg.Headers.User: w.Header().Get(cfg.Cfg.Headers.User)}).Debug("response header") @@ -424,6 +434,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 +581,77 @@ 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 + user.IDToken = string(tokenRes.IDToken) + user.AccessToken = string(tokenRes.AccessToken) + 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..28a31186 100644 --- a/pkg/cfg/cfg.go +++ b/pkg/cfg/cfg.go @@ -42,6 +42,8 @@ type config struct { } Headers struct { JWT string `mapstructure:"jwt"` + IDToken string `mapstructure:"idToken"` + AccessToken string `mapstructure:"accessToken"` User string `mapstructure:"user"` QueryString string `mapstructure:"querystring"` Redirect string `mapstructure:"redirect"` @@ -79,6 +81,7 @@ type OAuthProviders struct { Google string GitHub string IndieAuth string + ADFS string OIDC string } @@ -113,6 +116,7 @@ var ( Google: "google", GitHub: "github", IndieAuth: "indieauth", + ADFS: "adfs", OIDC: "oidc", } @@ -236,8 +240,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") } @@ -352,6 +356,12 @@ func setDefaults() { if !viper.IsSet(Branding.LCName + ".headers.jwt") { Cfg.Headers.JWT = "X-" + Branding.CcName + "-Token" } + if !viper.IsSet(Branding.LCName + ".headers.idToken") { + Cfg.Headers.IDToken = "" + } + if !viper.IsSet(Branding.LCName + ".headers.accessToken") { + Cfg.Headers.AccessToken = "" + } if !viper.IsSet(Branding.LCName + ".headers.querystring") { Cfg.Headers.QueryString = "access_token" } @@ -404,6 +414,9 @@ func setDefaults() { } else if GenOAuth.Provider == Providers.GitHub { setDefaultsGitHub() configureOAuthClient() + } else if GenOAuth.Provider == Providers.ADFS { + setDefaultsADFS() + configureOAuthClient() } else { configureOAuthClient() } @@ -429,6 +442,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/jwtmanager/jwtmanager.go b/pkg/jwtmanager/jwtmanager.go index f6f7558e..925c2aae 100644 --- a/pkg/jwtmanager/jwtmanager.go +++ b/pkg/jwtmanager/jwtmanager.go @@ -21,8 +21,10 @@ import ( // VouchClaims jwt Claims specific to vouch type VouchClaims struct { - Username string `json:"username"` - Sites []string `json:"sites"` // tempting to make this a map but the array is fewer characters in the jwt + Username string `json:"username"` + Sites []string `json:"sites"` // tempting to make this a map but the array is fewer characters in the jwt + IDToken string `json:"id_token"` + AccessToken string `json:"access_token"` jwt.StandardClaims } @@ -53,6 +55,8 @@ func CreateUserTokenString(u structs.User) string { claims := VouchClaims{ u.Username, Sites, + u.IDToken, + u.AccessToken, StandardClaims, } diff --git a/pkg/jwtmanager/jwtmanager_test.go b/pkg/jwtmanager/jwtmanager_test.go index 95a3d873..5c57b3ff 100644 --- a/pkg/jwtmanager/jwtmanager_test.go +++ b/pkg/jwtmanager/jwtmanager_test.go @@ -26,6 +26,8 @@ func init() { lc = VouchClaims{ u1.Username, Sites, + u1.IDToken, + u1.AccessToken, StandardClaims, } } diff --git a/pkg/structs/structs.go b/pkg/structs/structs.go index 62ab2481..53b18250 100644 --- a/pkg/structs/structs.go +++ b/pkg/structs/structs.go @@ -10,12 +10,14 @@ type User struct { // TODO: set Provider here so that we can pass it to db // populated by db (via mapstructure) or from provider (via json) // Provider string `json:"provider",mapstructure:"provider"` - Username string `json:"username",mapstructure:"username"` - Name string `json:"name",mapstructure:"name"` - Email string `json:"email",mapstructure:"email"` - CreatedOn int64 `json:"createdon"` - LastUpdate int64 `json:"lastupdate"` - ID int `json:"id",mapstructure:"id"` + Username string `json:"username",mapstructure:"username"` + Name string `json:"name",mapstructure:"name"` + Email string `json:"email",mapstructure:"email"` + CreatedOn int64 `json:"createdon"` + LastUpdate int64 `json:"lastupdate"` + ID int `json:"id",mapstructure:"id"` + IDToken string `json:"id_token",mapstructure:"id_token"` + AccessToken string `json:"access_token,mapstructure:"id_token"` // jwt.StandardClaims } @@ -47,6 +49,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