pkg/provider/netiq/netiq.go (182 lines of code) (raw):
package netiq
import (
"fmt"
"net/http"
"net/url"
"regexp"
"strings"
"github.com/PuerkitoBio/goquery"
"github.com/aliyun/saml2alibabacloud/pkg/cfg"
"github.com/aliyun/saml2alibabacloud/pkg/creds"
"github.com/aliyun/saml2alibabacloud/pkg/page"
"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", "NetIQ")
type Client struct {
client *provider.HTTPClient
MFA string
}
// New creates a new external client
func New(idpAccount *cfg.IDPAccount, mfa string) (*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, MFA: mfa}, nil
}
const samlURL = "/nidp/saml2/idpsend?PID=STSPv8a5kc"
func (nc *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error) {
req, err := http.NewRequest("GET", loginDetails.URL+samlURL, nil)
if err != nil {
return "", errors.Wrap(err, "Error building request")
}
return nc.follow(req, loginDetails)
}
func (nc *Client) follow(req *http.Request, loginDetails *creds.LoginDetails) (string, error) {
resp, err := nc.client.Do(req)
if err != nil {
return "", errors.Wrap(err, "Failed to perform http request to "+req.URL.String())
}
doc, err := goquery.NewDocumentFromReader(resp.Body)
if err != nil {
return "", errors.Wrap(err, "failed to build document from response")
}
if isSAMLResponse(doc) {
return extractSAMLAssertion(doc)
} else if resourcePath, isGetToContext := extractGetToContentUrl(doc); isGetToContext {
loginUrl, err := getLoginUrl(nc.MFA, loginDetails.URL, resourcePath)
if err != nil {
return "", errors.Wrap(err, "MFA option unsupported. Valid MFA options are: Auto or Privileged")
}
newReq, err := buildGetToContentRequest(loginUrl + "&uiDestination=contentDiv")
if err != nil {
return "", errors.Wrap(err, "Error building request")
}
return nc.follow(newReq, loginDetails)
} else if resourceURL, isWinLocHref := extractWinLocHrefURL(doc); isWinLocHref {
newReq, err := buildGetToContentRequest(resourceURL)
if err != nil {
return "", errors.Wrap(err, "Error building request")
}
return nc.follow(newReq, loginDetails)
} else if form, isIDPLoginPass := extractIDPLoginPass(doc); isIDPLoginPass {
form.Values.Set("Ecom_User_ID", loginDetails.Username)
form.Values.Set("Ecom_Password", loginDetails.Password)
newReq, err := form.BuildRequest()
if err != nil {
return "", errors.Wrap(err, "Error building request")
}
return nc.follow(newReq, loginDetails)
} else if form, isIDPLoginRsa := extractIDPLoginRsa(doc); isIDPLoginRsa {
token := prompter.StringRequired("Enter concatenated pin and token")
form.Values.Set("Ecom_User_ID", loginDetails.Username)
form.Values.Set("Ecom_Token", token)
newReq, err := form.BuildRequest()
if err != nil {
return "", errors.Wrap(err, "Error building request")
}
return nc.follow(newReq, loginDetails)
} else {
return "", fmt.Errorf("unknown document type")
}
}
func isSAMLResponse(doc *goquery.Document) bool {
return doc.Find("input[name=\"SAMLResponse\"]").Size() == 1
}
func extractSAMLAssertion(doc *goquery.Document) (string, error) {
samlAssertion, ok := doc.Find("input[name=\"SAMLResponse\"]").Attr("value")
if !ok {
return "", fmt.Errorf("no SAML assertion in response")
}
logDocDetected("samlResponse", samlAssertion)
return samlAssertion, nil
}
func extractGetToContentUrl(doc *goquery.Document) (string, bool) {
script := doc.Find("body script:contains('getToContent')")
if script.Size() != 1 {
return "", false
}
re := regexp.MustCompile(`getToContent\('(.*)',.*\);`)
match := re.FindStringSubmatch(strings.TrimSpace(script.Text()))
if len(match) == 2 {
logDocDetected("getToContent", match[1])
return match[1], true
} else {
return "", false
}
}
func extractWinLocHrefURL(doc *goquery.Document) (string, bool) {
script := doc.Find("body script:contains('window.location.href')")
if script.Size() != 1 {
return "", false
}
re := regexp.MustCompile(`window.location.href='(.*)';`)
match := re.FindStringSubmatch(strings.TrimSpace(script.Text()))
if len(match) == 2 {
logDocDetected("winLocHref", match[1])
return match[1], true
} else {
return "", false
}
}
func extractIDPLoginPass(doc *goquery.Document) (*page.Form, bool) {
idpPoginForm := doc.Find("body form:has(input[name=\"Ecom_Password\"])")
if idpPoginForm.Size() != 1 {
return nil, false
}
action, exists := idpPoginForm.Attr("action")
if !exists {
return nil, false
}
logDocDetected("idpLoginPass", action)
form := &page.Form{
URL: action,
Method: "POST",
Values: &url.Values{},
}
return form, true
}
func extractIDPLoginRsa(doc *goquery.Document) (*page.Form, bool) {
idpPoginForm := doc.Find("body form:has(input[name=\"Ecom_Token\"])")
if idpPoginForm.Size() != 1 {
return nil, false
}
action, exists := idpPoginForm.Attr("action")
if !exists {
return nil, false
}
logDocDetected("idpLoginRsa", action)
form := &page.Form{
URL: action,
Method: "POST",
Values: &url.Values{},
}
return form, true
}
func buildGetToContentRequest(resourceURL string) (*http.Request, error) {
return http.NewRequest("GET", resourceURL, nil)
}
func logDocDetected(docType string, data string) {
logDetect := logger
if data != "" {
logDetect = logDetect.WithField("docType", docType)
}
if data != "" {
logDetect = logDetect.WithField("data", data)
}
logDetect.Debug("doc detect")
}
func getLoginUrl(mfa string, baseUrl string, defaultResourcePath string) (string, error) {
var loginUrl string
if mfa == "Auto" {
loginUrl = baseUrl + defaultResourcePath
} else if mfa == "Privileged" {
// Privileged account skip MFA and have different login URL
loginUrl = baseUrl + "/nidp/app/login?id=privacc&sid=0&option=credential"
} else {
return "", errors.New("Unsupported MFA")
}
return loginUrl, nil
}