pkg/provider/f5apm/f5apm.go (239 lines of code) (raw):
package f5apm
import (
"bytes"
"encoding/base64"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
"github.com/PuerkitoBio/goquery"
"github.com/aliyun/saml2alibabacloud/pkg/cfg"
"github.com/aliyun/saml2alibabacloud/pkg/creds"
"github.com/aliyun/saml2alibabacloud/pkg/dump"
"github.com/aliyun/saml2alibabacloud/pkg/prompter"
"github.com/aliyun/saml2alibabacloud/pkg/provider"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
var logger = logrus.WithField("provider", "f5apm")
//Client client for F5 APM
type Client struct {
client *provider.HTTPClient
policyID string
}
// New create new F5 APM client
func New(idpAccount *cfg.IDPAccount) (*Client, error) {
tr := provider.NewDefaultTransport(idpAccount.SkipVerify)
client, err := provider.NewHTTPClient(tr, provider.BuildHttpClientOpts(idpAccount))
if err != nil {
return nil, errors.Wrap(err, "Error building HTTP client")
}
return &Client{client: client, policyID: idpAccount.ResourceID}, nil
}
// Authenticate logs into F5 APM and returns a SAML response
func (ac *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error) {
logger.Debug("Get Login Form")
logger.Debugf("Login URL: %s", loginDetails.URL)
logger.Debugf("Login Username: %s", loginDetails.Username)
authForm, err := ac.getLoginForm(loginDetails)
if err != nil {
return "", errors.Wrap(err, "Error getting login form IDP")
}
// Post username/password
logger.Debug("Post UP Login Form")
debugAuthForm(authForm)
upData, err := ac.postLoginForm(loginDetails, authForm)
if err != nil {
return "", errors.Wrap(err, "Error submitting login form")
}
upDoc, err := goquery.NewDocumentFromReader(bytes.NewBuffer(upData))
if err != nil {
return "", errors.Wrap(err, "Error reading UP data")
}
mfaFound, mfaMethods := containsMFAForm(upDoc)
// Prompt for MFA if needed
if mfaFound {
logger.Debug(mfaMethods)
mfaAuthForm := url.Values{}
var mfaToken string
mfaMethod, err := prompter.ChooseWithDefault("MFA Method", mfaMethods[0], mfaMethods)
if err != nil {
return "", errors.Wrap(err, "Error selecting MFA method")
}
switch mfaMethod {
case "token":
mfaToken = prompter.RequestSecurityCode("000000")
case "push":
mfaToken = ""
}
// Post mfatoken
mfaAuthForm.Add("mfatoken", mfaToken)
mfaAuthForm.Add("mfamethod", mfaMethod)
mfaAuthForm.Add("mfa_retry", "")
logger.Debug("Post Token Form")
debugAuthForm(mfaAuthForm)
_, err = ac.postLoginForm(loginDetails, mfaAuthForm)
if err != nil {
return "", errors.Wrap(err, "Error submitting MFA login form")
}
}
// Post to saml endpoint
logger.Debug("Get SAML Form")
samlAssertion, err := ac.getSAMLAssertion(loginDetails)
if err != nil {
return "", errors.Wrap(err, "Error getting saml assertion")
}
decodedAssertion, err := base64.StdEncoding.DecodeString(samlAssertion)
if err != nil {
return "", errors.Wrap(err, "Error decoding saml assertion")
}
if dump.ContentEnable() {
logger.Debugf("SAMLAssertion: %s", string(decodedAssertion))
}
return samlAssertion, nil
}
func (ac *Client) getSAMLAssertion(loginDetails *creds.LoginDetails) (string, error) {
req, err := http.NewRequest("GET", fmt.Sprintf("%s/saml/idp/res", loginDetails.URL), nil)
if err != nil {
return "", errors.Wrap(err, "Error building SAML assertion request")
}
debugHTTPRequest(ac, req)
// Don't urlencode query string - APM bug
req.URL.RawQuery = fmt.Sprintf("id=%s", ac.policyID)
res, err := ac.client.Do(req)
if err != nil {
return "", errors.Wrap(err, "Error retrieving SAML assertion request")
}
debugHTTPResponse(ac, res)
samlData, err := ioutil.ReadAll(res.Body)
if err != nil {
return "", errors.Wrap(err, "Error reading SAML assertion body")
}
var samlAssertion string
doc, err := goquery.NewDocumentFromReader(bytes.NewBuffer(samlData))
if err != nil {
return "", errors.Wrap(err, "Error reading SAML data")
}
doc.Find("input").Each(func(i int, s *goquery.Selection) {
name, ok := s.Attr("name")
if !ok {
logger.Fatalf("Unable to locate IDP authentication")
}
if name == "SAMLResponse" {
val, ok := s.Attr("value")
if !ok {
logger.Fatalf("Unable to locate SAML assertion value")
}
samlAssertion = val
}
})
return samlAssertion, nil
}
func (ac *Client) getLoginForm(loginDetails *creds.LoginDetails) (url.Values, error) {
req, err := http.NewRequest("GET", loginDetails.URL, nil)
if err != nil {
return nil, errors.Wrap(err, "Error building get loging form request")
}
debugHTTPRequest(ac, req)
res, err := ac.client.Do(req)
if err != nil {
return nil, errors.Wrap(err, "Error retrieving login form")
}
debugHTTPResponse(ac, res)
doc, err := goquery.NewDocumentFromReader(res.Body)
if err != nil {
return nil, errors.Wrap(err, "Failed to build document from response")
}
authForm := url.Values{}
doc.Find("input").Each(func(i int, s *goquery.Selection) {
name, ok := s.Attr("name")
if !ok {
return
}
lname := strings.ToLower(name)
if strings.Contains(lname, "username") {
authForm.Add(name, loginDetails.Username)
} else if strings.Contains(lname, "password") {
authForm.Add(name, loginDetails.Password)
} else {
val, ok := s.Attr("value")
if !ok {
return
}
authForm.Add(name, val)
}
})
return authForm, nil
}
func (ac *Client) postLoginForm(loginDetails *creds.LoginDetails, authForm url.Values) ([]byte, error) {
logger.Debug("Auth Post")
req, err := http.NewRequest("POST", fmt.Sprintf("%s/my.policy", loginDetails.URL), strings.NewReader(authForm.Encode()))
if err != nil {
return nil, errors.Wrap(err, "Error building authentication request")
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
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")
req.Header.Set("Accept", "*/*")
req.Header.Add("Referer", fmt.Sprintf("%s/my.policy", loginDetails.URL))
if authForm.Get("mfamethod") != "" {
req.AddCookie(&http.Cookie{Name: "f5cid00", Value: "token"})
}
debugHTTPRequest(ac, req)
res, err := ac.client.Do(req)
if err != nil {
return nil, errors.Wrap(err, "Error retrieving login form")
}
debugHTTPResponse(ac, res)
data, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, errors.Wrap(err, "Error reading response body")
}
return data, nil
}
func debugAuthForm(vals url.Values) {
for key, values := range vals {
if strings.ToLower(key) == "password" {
values = []string{"XXXXXXXXX"}
}
logger.Debugf("%-20s %-18s: %-40s", "Auth Form:", key, strings.Join(values, ", "))
}
}
func debugHTTPRequest(ac *Client, req *http.Request) {
logger.Debug(dump.RequestString(req))
logger.Debug(req.URL)
for name, values := range req.Header {
logger.Debugf("%-20s %-18s: %-40s", fmt.Sprintf("%s Request Header:", req.Method), name, strings.Join(values, ", "))
}
for _, reqCookie := range ac.client.Jar.Cookies(req.URL) {
logger.Debugf("%-20s %-18s: %-40s %s", fmt.Sprintf("%s Request Cookie:", req.Method), reqCookie.Name, reqCookie.Value, reqCookie.Domain)
}
}
func debugHTTPResponse(ac *Client, res *http.Response) {
logger.Debug(dump.ResponseString(res))
logger.Debug(res.Request.URL)
for name, values := range res.Header {
logger.Debugf("%-20s %-18s: %-40s", fmt.Sprintf("%s Response Header:", res.Request.Method), name, strings.Join(values, ", "))
}
for _, resCookie := range ac.client.Jar.Cookies(res.Request.URL) {
logger.Debugf("%-20s %-18s: %-40s %s", fmt.Sprintf("%s Response Cookie:", res.Request.Method), resCookie.Name, resCookie.Value, resCookie.Domain)
}
}
func containsMFAForm(doc *goquery.Document) (bool, []string) {
containsMFA := false
var mfaMethods []string
// Look for a form input ID named "mfa_retry"
doc.Find("input").Each(func(i int, s *goquery.Selection) {
id, _ := s.Attr("id")
if strings.Contains(id, "mfa_retry") {
containsMFA = true
}
})
doc.Find("select").Each(func(i int, s *goquery.Selection) {
name, _ := s.Attr("name")
if strings.Contains(name, "mfamethod") {
s.Find("option").Each(func(i int, opt *goquery.Selection) {
option, _ := opt.Attr("value")
logger.Debugf("MFA options: %s", option)
mfaMethods = append(mfaMethods, option)
})
}
})
if len(mfaMethods) == 0 {
return false, nil
}
logger.Debugf("MFA Form: '%#v'", containsMFA)
logger.Debugf("MFA Methods: '%#v'", mfaMethods)
return containsMFA, mfaMethods
}