github.com/versent/saml2aws@v2.17.0+incompatible/pkg/provider/keycloak/keycloak.go (about)

     1  package keycloak
     2  
     3  import (
     4  	"bytes"
     5  	"io/ioutil"
     6  	"log"
     7  	"net/http"
     8  	"net/url"
     9  	"strings"
    10  
    11  	"github.com/PuerkitoBio/goquery"
    12  	"github.com/pkg/errors"
    13  	"github.com/sirupsen/logrus"
    14  	"github.com/versent/saml2aws/pkg/cfg"
    15  	"github.com/versent/saml2aws/pkg/creds"
    16  	"github.com/versent/saml2aws/pkg/prompter"
    17  	"github.com/versent/saml2aws/pkg/provider"
    18  
    19  	"fmt"
    20  )
    21  
    22  var logger = logrus.WithField("provider", "keycloak")
    23  
    24  // Client wrapper around KeyCloak.
    25  type Client struct {
    26  	client *provider.HTTPClient
    27  }
    28  
    29  // New create a new KeyCloakClient
    30  func New(idpAccount *cfg.IDPAccount) (*Client, error) {
    31  
    32  	tr := provider.NewDefaultTransport(idpAccount.SkipVerify)
    33  
    34  	client, err := provider.NewHTTPClient(tr)
    35  	if err != nil {
    36  		return nil, errors.Wrap(err, "error building http client")
    37  	}
    38  
    39  	return &Client{
    40  		client: client,
    41  	}, nil
    42  }
    43  
    44  // Authenticate logs into KeyCloak and returns a SAML response
    45  func (kc *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error) {
    46  
    47  	authSubmitURL, authForm, err := kc.getLoginForm(loginDetails)
    48  	if err != nil {
    49  		return "", errors.Wrap(err, "error retrieving login form from idp")
    50  	}
    51  
    52  	data, err := kc.postLoginForm(authSubmitURL, authForm)
    53  	if err != nil {
    54  		return "", fmt.Errorf("error submitting login form")
    55  	}
    56  	if authSubmitURL == "" {
    57  		return "", fmt.Errorf("error submitting login form")
    58  	}
    59  
    60  	doc, err := goquery.NewDocumentFromReader(bytes.NewBuffer(data))
    61  	if err != nil {
    62  		return "", errors.Wrap(err, "error parsing document")
    63  	}
    64  
    65  	if containsTotpForm(doc) {
    66  		totpSubmitURL, err := extractSubmitURL(doc)
    67  		if err != nil {
    68  			return "", errors.Wrap(err, "unable to locate IDP totp form submit URL")
    69  		}
    70  
    71  		doc, err = kc.postTotpForm(totpSubmitURL, loginDetails.MFAToken, doc)
    72  		if err != nil {
    73  			return "", errors.Wrap(err, "error posting totp form")
    74  		}
    75  	}
    76  
    77  	var samlAssertion string
    78  
    79  	doc.Find("input").Each(func(i int, s *goquery.Selection) {
    80  		name, ok := s.Attr("name")
    81  		if !ok {
    82  			log.Fatalf("unable to locate IDP authentication form submit URL")
    83  		}
    84  		if name == "SAMLResponse" {
    85  			val, ok := s.Attr("value")
    86  			if !ok {
    87  				log.Fatalf("unable to locate saml assertion value")
    88  			}
    89  			samlAssertion = val
    90  		}
    91  	})
    92  
    93  	return samlAssertion, nil
    94  }
    95  
    96  func (kc *Client) getLoginForm(loginDetails *creds.LoginDetails) (string, url.Values, error) {
    97  
    98  	res, err := kc.client.Get(loginDetails.URL)
    99  	if err != nil {
   100  		return "", nil, errors.Wrap(err, "error retrieving form")
   101  	}
   102  
   103  	doc, err := goquery.NewDocumentFromResponse(res)
   104  	if err != nil {
   105  		return "", nil, errors.Wrap(err, "failed to build document from response")
   106  	}
   107  
   108  	authForm := url.Values{}
   109  
   110  	doc.Find("input").Each(func(i int, s *goquery.Selection) {
   111  		updateKeyCloakFormData(authForm, s, loginDetails)
   112  	})
   113  
   114  	authSubmitURL, err := extractSubmitURL(doc)
   115  	if err != nil {
   116  		return "", nil, errors.Wrap(err, "unable to locate IDP authentication form submit URL")
   117  	}
   118  
   119  	return authSubmitURL, authForm, nil
   120  }
   121  
   122  func (kc *Client) postLoginForm(authSubmitURL string, authForm url.Values) ([]byte, error) {
   123  
   124  	req, err := http.NewRequest("POST", authSubmitURL, strings.NewReader(authForm.Encode()))
   125  	if err != nil {
   126  		return nil, errors.Wrap(err, "error building authentication request")
   127  	}
   128  
   129  	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
   130  
   131  	res, err := kc.client.Do(req)
   132  	if err != nil {
   133  		return nil, errors.Wrap(err, "error retrieving login form")
   134  	}
   135  
   136  	data, err := ioutil.ReadAll(res.Body)
   137  	if err != nil {
   138  		return nil, errors.Wrap(err, "error retrieving body")
   139  	}
   140  
   141  	return data, nil
   142  }
   143  
   144  func (kc *Client) postTotpForm(totpSubmitURL string, mfaToken string, doc *goquery.Document) (*goquery.Document, error) {
   145  
   146  	otpForm := url.Values{}
   147  
   148  	if mfaToken == "" {
   149  		mfaToken = prompter.RequestSecurityCode("000000")
   150  	}
   151  
   152  	doc.Find("input").Each(func(i int, s *goquery.Selection) {
   153  		updateOTPFormData(otpForm, s, mfaToken)
   154  	})
   155  
   156  	req, err := http.NewRequest("POST", totpSubmitURL, strings.NewReader(otpForm.Encode()))
   157  	if err != nil {
   158  		return nil, errors.Wrap(err, "error building MFA request")
   159  	}
   160  
   161  	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
   162  
   163  	res, err := kc.client.Do(req)
   164  	if err != nil {
   165  		return nil, errors.Wrap(err, "error retrieving content")
   166  	}
   167  
   168  	doc, err = goquery.NewDocumentFromResponse(res)
   169  	if err != nil {
   170  		return nil, errors.Wrap(err, "error reading totp form response")
   171  	}
   172  
   173  	return doc, nil
   174  }
   175  
   176  func extractSubmitURL(doc *goquery.Document) (string, error) {
   177  
   178  	var submitURL string
   179  
   180  	doc.Find("form").Each(func(i int, s *goquery.Selection) {
   181  		action, ok := s.Attr("action")
   182  		if !ok {
   183  			return
   184  		}
   185  		submitURL = action
   186  	})
   187  
   188  	if submitURL == "" {
   189  		return "", fmt.Errorf("unable to locate form submit URL")
   190  	}
   191  
   192  	return submitURL, nil
   193  }
   194  
   195  func containsTotpForm(doc *goquery.Document) bool {
   196  	totpIndex := doc.Find("input#totp").Index()
   197  
   198  	if totpIndex != -1 {
   199  		return true
   200  	}
   201  
   202  	return false
   203  }
   204  
   205  func updateKeyCloakFormData(authForm url.Values, s *goquery.Selection, user *creds.LoginDetails) {
   206  	name, ok := s.Attr("name")
   207  	// log.Printf("name = %s ok = %v", name, ok)
   208  	if !ok {
   209  		return
   210  	}
   211  	lname := strings.ToLower(name)
   212  	if strings.Contains(lname, "username") {
   213  		authForm.Add(name, user.Username)
   214  	} else if strings.Contains(lname, "password") {
   215  		authForm.Add(name, user.Password)
   216  	} else {
   217  		// pass through any hidden fields
   218  		val, ok := s.Attr("value")
   219  		if !ok {
   220  			return
   221  		}
   222  		authForm.Add(name, val)
   223  	}
   224  }
   225  
   226  func updateOTPFormData(otpForm url.Values, s *goquery.Selection, token string) {
   227  	name, ok := s.Attr("name")
   228  	//	log.Printf("name = %s ok = %v", name, ok)
   229  	if !ok {
   230  		return
   231  	}
   232  	lname := strings.ToLower(name)
   233  	if strings.Contains(lname, "totp") {
   234  		otpForm.Add(name, token)
   235  	}
   236  
   237  }