forked from Versent/saml2aws
-
Notifications
You must be signed in to change notification settings - Fork 0
/
pingfed.go
252 lines (204 loc) · 6.35 KB
/
pingfed.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
package saml2aws
import (
"bytes"
"crypto/tls"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/http/cookiejar"
"net/url"
"strings"
"github.com/PuerkitoBio/goquery"
"github.com/pkg/errors"
"golang.org/x/net/publicsuffix"
)
// PingFedClient wrapper around PingFed + PingId enabling authentication and retrieval of assertions
type PingFedClient struct {
client *http.Client
}
// NewPingFedClient create a new PingFed client
func NewPingFedClient(skipVerify bool) (*PingFedClient, error) {
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: skipVerify},
}
options := &cookiejar.Options{
PublicSuffixList: publicsuffix.List,
}
jar, err := cookiejar.New(options)
if err != nil {
return nil, err
}
client := &http.Client{Transport: tr, Jar: jar}
//disable default behaviour to follow redirects as we use this to detect mfa
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return errors.New("Redirect")
}
return &PingFedClient{
client: client,
}, nil
}
// Authenticate Authenticate to PingFed and return the data from the body of the SAML assertion.
func (ac *PingFedClient) Authenticate(loginDetails *LoginDetails) (string, error) {
var authSubmitURL string
var samlAssertion string
mfaRequired := false
authForm := url.Values{}
pingFedURL := fmt.Sprintf("https://%s/idp/startSSO.ping?PartnerSpId=urn:amazon:webservices", loginDetails.Hostname)
res, err := ac.client.Get(pingFedURL)
if err != nil {
return samlAssertion, errors.Wrap(err, "error retieving form")
}
doc, err := goquery.NewDocumentFromResponse(res)
if err != nil {
return samlAssertion, errors.Wrap(err, "failed to build document from response")
}
doc.Find("input").Each(func(i int, s *goquery.Selection) {
updateLoginFormData(authForm, s, loginDetails)
})
//spew.Dump(authForm)
doc.Find("form").Each(func(i int, s *goquery.Selection) {
action, ok := s.Attr("action")
if !ok {
return
}
authSubmitURL = action
})
if authSubmitURL == "" {
return samlAssertion, fmt.Errorf("unable to locate IDP authentication form submit URL")
}
authSubmitURL = fmt.Sprintf("https://%s%s", loginDetails.Hostname, authSubmitURL)
//log.Printf("id authentication url: %s", authSubmitURL)
req, err := http.NewRequest("POST", authSubmitURL, strings.NewReader(authForm.Encode()))
if err != nil {
return samlAssertion, errors.Wrap(err, "error building authentication request")
}
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
res, err = ac.client.Do(req)
if err != nil {
//check for redirect, this indicates PingOne MFA being used
if res.StatusCode == 302 {
mfaRequired = true
} else {
return samlAssertion, errors.Wrap(err, "error retieving login form")
}
}
//process mfa
if mfaRequired {
mfaURL, err := res.Location()
//spew.Dump(mfaURL)
//follow redirect
res, err = ac.client.Get(mfaURL.String())
if err != nil {
return samlAssertion, errors.Wrap(err, "error retieving form")
}
//extract form action and jwt token
form, actionURL, err := extractFormData(res)
//request mfa auth via PingId (device swipe)
req, err := http.NewRequest("POST", actionURL, strings.NewReader(form.Encode()))
if err != nil {
return samlAssertion, errors.Wrap(err, "error building mfa authentication request")
}
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
res, err = ac.client.Do(req)
if err != nil {
return samlAssertion, errors.Wrap(err, "error retieving mfa response")
}
//extract form action and csrf token
form, actionURL, err = extractFormData(res)
//contine mfa auth with csrf token
req, err = http.NewRequest("POST", actionURL, strings.NewReader(form.Encode()))
if err != nil {
return samlAssertion, errors.Wrap(err, "error building authentication request")
}
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
res, err = ac.client.Do(req)
if err != nil {
return samlAssertion, errors.Wrap(err, "error polling mfa device")
}
//extract form action and jwt token
form, actionURL, err = extractFormData(res)
//pass PingId auth back to pingfed
req, err = http.NewRequest("POST", actionURL, strings.NewReader(form.Encode()))
if err != nil {
return samlAssertion, errors.Wrap(err, "error building authentication request")
}
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
res, err = ac.client.Do(req)
if err != nil {
return samlAssertion, errors.Wrap(err, "error authenticating mfa")
}
}
//log.Printf("res code = %v status = %s", res.StatusCode, res.Status)
data, err := ioutil.ReadAll(res.Body)
if err != nil {
return samlAssertion, errors.Wrap(err, "error retieving body")
}
doc, err = goquery.NewDocumentFromReader(bytes.NewBuffer(data))
if err != nil {
return samlAssertion, errors.Wrap(err, "error parsing document")
}
doc.Find("input").Each(func(i int, s *goquery.Selection) {
name, ok := s.Attr("name")
if !ok {
log.Fatalf("unable to locate IDP authentication form submit URL")
}
if name == "SAMLResponse" {
val, ok := s.Attr("value")
if !ok {
log.Fatalf("unable to locate saml assertion value")
}
samlAssertion = val
}
})
return samlAssertion, nil
}
func updateLoginFormData(authForm url.Values, s *goquery.Selection, user *LoginDetails) {
name, ok := s.Attr("name")
// log.Printf("name = %s ok = %v", name, ok)
if !ok {
return
}
lname := strings.ToLower(name)
if strings.Contains(lname, "pf.username") {
authForm.Add(name, user.Username)
} else if strings.Contains(lname, "pf.pass") {
authForm.Add(name, user.Password)
} else {
// pass through any hidden fields
val, ok := s.Attr("value")
if !ok {
return
}
authForm.Add(name, val)
}
}
func extractFormData(res *http.Response) (url.Values, string, error) {
formData := url.Values{}
var actionURL string
doc, err := goquery.NewDocumentFromResponse(res)
if err != nil {
return formData, actionURL, errors.Wrap(err, "failed to build document from response")
}
//get action url
doc.Find("form").Each(func(i int, s *goquery.Selection) {
action, ok := s.Attr("action")
if !ok {
return
}
actionURL = action
})
// exxtract form data to passthrough
doc.Find("input").Each(func(i int, s *goquery.Selection) {
name, ok := s.Attr("name")
if !ok {
return
}
val, ok := s.Attr("value")
if !ok {
return
}
formData.Add(name, val)
})
return formData, actionURL, nil
}