github.com/versent/saml2aws@v2.17.0+incompatible/pkg/provider/f5apm/f5apm.go (about) 1 package f5apm 2 3 import ( 4 "bytes" 5 "encoding/base64" 6 "fmt" 7 "io/ioutil" 8 "net/http" 9 "net/url" 10 "strings" 11 12 "github.com/PuerkitoBio/goquery" 13 14 "github.com/versent/saml2aws/pkg/cfg" 15 "github.com/versent/saml2aws/pkg/creds" 16 "github.com/versent/saml2aws/pkg/dump" 17 "github.com/versent/saml2aws/pkg/prompter" 18 19 "github.com/pkg/errors" 20 "github.com/versent/saml2aws/pkg/provider" 21 22 "github.com/sirupsen/logrus" 23 ) 24 25 var logger = logrus.WithField("provider", "f5apm") 26 27 //Client client for F5 APM 28 type Client struct { 29 client *provider.HTTPClient 30 policyID string 31 } 32 33 // New create new F5 APM client 34 func New(idpAccount *cfg.IDPAccount) (*Client, error) { 35 36 tr := provider.NewDefaultTransport(idpAccount.SkipVerify) 37 client, err := provider.NewHTTPClient(tr) 38 if err != nil { 39 return nil, errors.Wrap(err, "Error building HTTP client") 40 } 41 return &Client{client: client, policyID: idpAccount.ResourceID}, nil 42 } 43 44 // Authenticate logs into F5 APM and returns a SAML response 45 func (ac *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error) { 46 logger.Debug("Get Login Form") 47 logger.Debugf("Login URL: %s", loginDetails.URL) 48 logger.Debugf("Login Username: %s", loginDetails.Username) 49 authForm, err := ac.getLoginForm(loginDetails) 50 if err != nil { 51 return "", errors.Wrap(err, "Error getting login form IDP") 52 } 53 54 // Post username/password 55 logger.Debug("Post UP Login Form") 56 debugAuthForm(authForm) 57 58 upData, err := ac.postLoginForm(loginDetails, authForm) 59 if err != nil { 60 return "", errors.Wrap(err, "Error submitting login form") 61 } 62 63 upDoc, err := goquery.NewDocumentFromReader(bytes.NewBuffer(upData)) 64 mfaFound, mfaMethods := containsMFAForm(upDoc) 65 66 // Prompt for MFA if needed 67 if mfaFound { 68 logger.Debug(mfaMethods) 69 mfaAuthForm := url.Values{} 70 var mfaToken string 71 mfaMethod, err := prompter.ChooseWithDefault("MFA Method", mfaMethods[0], mfaMethods) 72 if err != nil { 73 return "", errors.Wrap(err, "Error selecting MFA method") 74 } 75 switch mfaMethod { 76 case "token": 77 mfaToken = prompter.RequestSecurityCode("000000") 78 case "push": 79 mfaToken = "" 80 } 81 // Post mfatoken 82 mfaAuthForm.Add("mfatoken", mfaToken) 83 mfaAuthForm.Add("mfamethod", mfaMethod) 84 mfaAuthForm.Add("mfa_retry", "") 85 logger.Debug("Post Token Form") 86 debugAuthForm(mfaAuthForm) 87 _, err = ac.postLoginForm(loginDetails, mfaAuthForm) 88 if err != nil { 89 return "", errors.Wrap(err, "Error submitting MFA login form") 90 } 91 } 92 93 // Post to saml endpoint 94 logger.Debug("Get SAML Form") 95 samlAssertion, err := ac.getSAMLAssertion(loginDetails) 96 if err != nil { 97 return "", errors.Wrap(err, "Error getting saml assertion") 98 } 99 decodedAssertion, err := base64.StdEncoding.DecodeString(samlAssertion) 100 if err != nil { 101 return "", errors.Wrap(err, "Error decoding saml assertion") 102 } 103 if dump.ContentEnable() { 104 logger.Debugf("SAMLAssertion: %s", string(decodedAssertion)) 105 106 } 107 return samlAssertion, nil 108 } 109 110 func (ac *Client) getSAMLAssertion(loginDetails *creds.LoginDetails) (string, error) { 111 req, err := http.NewRequest("GET", fmt.Sprintf("%s/saml/idp/res", loginDetails.URL), nil) 112 113 if err != nil { 114 return "", errors.Wrap(err, "Error building SAML assertion request") 115 } 116 debugHTTPRequest(ac, req) 117 // Don't urlencode query string - APM bug 118 req.URL.RawQuery = fmt.Sprintf("id=%s", ac.policyID) 119 res, err := ac.client.Do(req) 120 if err != nil { 121 return "", errors.Wrap(err, "Error retrieving SAML assertion request") 122 } 123 debugHTTPResponse(ac, res) 124 samlData, err := ioutil.ReadAll(res.Body) 125 if err != nil { 126 return "", errors.Wrap(err, "Error reading SAML asseration body") 127 } 128 var samlAssertion string 129 doc, err := goquery.NewDocumentFromReader(bytes.NewBuffer(samlData)) 130 doc.Find("input").Each(func(i int, s *goquery.Selection) { 131 name, ok := s.Attr("name") 132 if !ok { 133 logger.Fatalf("Unable to locate IDP authentication") 134 } 135 if name == "SAMLResponse" { 136 val, ok := s.Attr("value") 137 if !ok { 138 logger.Fatalf("Unable to locate SAML assertion value") 139 } 140 samlAssertion = val 141 } 142 }) 143 return samlAssertion, nil 144 } 145 146 func (ac *Client) getLoginForm(loginDetails *creds.LoginDetails) (url.Values, error) { 147 req, err := http.NewRequest("GET", loginDetails.URL, nil) 148 if err != nil { 149 return nil, errors.Wrap(err, "Error building get loging form request") 150 } 151 debugHTTPRequest(ac, req) 152 res, err := ac.client.Do(req) 153 if err != nil { 154 return nil, errors.Wrap(err, "Error retrieving login form") 155 } 156 debugHTTPResponse(ac, res) 157 158 doc, err := goquery.NewDocumentFromReader(res.Body) 159 if err != nil { 160 return nil, errors.Wrap(err, "Failed to build document from response") 161 } 162 authForm := url.Values{} 163 doc.Find("input").Each(func(i int, s *goquery.Selection) { 164 name, ok := s.Attr("name") 165 if !ok { 166 return 167 } 168 lname := strings.ToLower(name) 169 if strings.Contains(lname, "username") { 170 authForm.Add(name, loginDetails.Username) 171 } else if strings.Contains(lname, "password") { 172 authForm.Add(name, loginDetails.Password) 173 } else { 174 val, ok := s.Attr("value") 175 if !ok { 176 return 177 } 178 authForm.Add(name, val) 179 } 180 }) 181 return authForm, nil 182 } 183 184 func (ac *Client) postLoginForm(loginDetails *creds.LoginDetails, authForm url.Values) ([]byte, error) { 185 logger.Debug("Auth Post") 186 187 req, err := http.NewRequest("POST", fmt.Sprintf("%s/my.policy", loginDetails.URL), strings.NewReader(authForm.Encode())) 188 if err != nil { 189 return nil, errors.Wrap(err, "Error building authentication request") 190 } 191 req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 192 req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:65.Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:65.0) Gecko/20100101 Firefox/65.00) Gecko/20100101 Firefox/65.0") 193 req.Header.Set("Accept", "*/*") 194 195 req.Header.Add("Referer", fmt.Sprintf("%s/my.policy", loginDetails.URL)) 196 if authForm.Get("mfamethod") != "" { 197 req.AddCookie(&http.Cookie{Name: "f5cid00", Value: "token"}) 198 } 199 debugHTTPRequest(ac, req) 200 res, err := ac.client.Do(req) 201 if err != nil { 202 return nil, errors.Wrap(err, "Error retrieving login form") 203 } 204 debugHTTPResponse(ac, res) 205 206 data, err := ioutil.ReadAll(res.Body) 207 if err != nil { 208 return nil, errors.Wrap(err, "Error reading response body") 209 } 210 return data, nil 211 } 212 213 func debugAuthForm(vals url.Values) { 214 for key, values := range vals { 215 if strings.ToLower(key) == "password" { 216 values = []string{"XXXXXXXXX"} 217 } 218 logger.Debugf("%-20s %-18s: %-40s", "Auth Form:", key, strings.Join(values, ", ")) 219 } 220 } 221 222 func debugHTTPRequest(ac *Client, req *http.Request) { 223 logger.Debug(dump.RequestString(req)) 224 logger.Debug(req.URL) 225 for name, values := range req.Header { 226 logger.Debugf("%-20s %-18s: %-40s", fmt.Sprintf("%s Request Header:", req.Method), name, strings.Join(values, ", ")) 227 } 228 for _, reqCookie := range ac.client.Jar.Cookies(req.URL) { 229 logger.Debugf("%-20s %-18s: %-40s %s", fmt.Sprintf("%s Request Cookie:", req.Method), reqCookie.Name, reqCookie.Value, reqCookie.Domain) 230 } 231 232 } 233 func debugHTTPResponse(ac *Client, res *http.Response) { 234 logger.Debug(dump.ResponseString(res)) 235 logger.Debug(res.Request.URL) 236 for name, values := range res.Header { 237 logger.Debugf("%-20s %-18s: %-40s", fmt.Sprintf("%s Response Header:", res.Request.Method), name, strings.Join(values, ", ")) 238 } 239 for _, resCookie := range ac.client.Jar.Cookies(res.Request.URL) { 240 logger.Debugf("%-20s %-18s: %-40s %s", fmt.Sprintf("%s Response Cookie:", res.Request.Method), resCookie.Name, resCookie.Value, resCookie.Domain) 241 } 242 } 243 244 func containsMFAForm(doc *goquery.Document) (bool, []string) { 245 containsMFA := false 246 var mfaMethods []string 247 // Look for a form input ID named "mfa_retry" 248 doc.Find("input").Each(func(i int, s *goquery.Selection) { 249 id, _ := s.Attr("id") 250 if strings.Contains(id, "mfa_retry") { 251 containsMFA = true 252 } 253 }) 254 doc.Find("select").Each(func(i int, s *goquery.Selection) { 255 name, _ := s.Attr("name") 256 if strings.Contains(name, "mfamethod") { 257 s.Find("option").Each(func(i int, opt *goquery.Selection) { 258 option, _ := opt.Attr("value") 259 logger.Debugf("MFA options: %s", option) 260 mfaMethods = append(mfaMethods, option) 261 }) 262 } 263 }) 264 if len(mfaMethods) == 0 { 265 return false, nil 266 } 267 logger.Debugf("MFA Form: '%#v'", containsMFA) 268 logger.Debugf("MFA Methods: '%#v'", mfaMethods) 269 return containsMFA, mfaMethods 270 }