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 }