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  }