forked from Versent/saml2aws
-
Notifications
You must be signed in to change notification settings - Fork 0
/
jumpcloud.go
192 lines (158 loc) · 5.15 KB
/
jumpcloud.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
package jumpcloud
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"regexp"
"strings"
"github.com/PuerkitoBio/goquery"
"github.com/pkg/errors"
"github.com/versent/saml2aws/pkg/cfg"
"github.com/versent/saml2aws/pkg/creds"
"github.com/versent/saml2aws/pkg/prompter"
"github.com/versent/saml2aws/pkg/provider"
)
const (
jcSSOBaseURL = "https://sso.jumpcloud.com/"
xsrfURL = "https://console.jumpcloud.com/userconsole/xsrf"
authSubmitURL = "https://console.jumpcloud.com/userconsole/auth"
)
// Client is a wrapper representing a JumpCloud SAML client
type Client struct {
client *provider.HTTPClient
}
// XSRF is for unmarshalling the xsrf token in the response
type XSRF struct {
Token string `json:"xsrf"`
}
// AuthRequest is to be sent to JumpCloud as the auth req body
type AuthRequest struct {
Context string
RedirectTo string
Email string
Password string
OTP string
}
// JCRedirect is for unmarshalling the redirect address from the response after the auth
type JCRedirect struct {
Address string `json:"redirectTo"`
}
// New creates a new JumpCloud client
func New(idpAccount *cfg.IDPAccount) (*Client, error) {
tr := provider.NewDefaultTransport(idpAccount.SkipVerify)
client, err := provider.NewHTTPClient(tr)
if err != nil {
return nil, errors.Wrap(err, "error building http client")
}
return &Client{
client: client,
}, nil
}
// Authenticate logs into JumpCloud and returns a SAML response
func (jc *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error) {
var samlAssertion string
var a AuthRequest
re := regexp.MustCompile(jcSSOBaseURL)
// Start by getting the XSRF Token
res, err := jc.client.Get(xsrfURL)
if err != nil {
return samlAssertion, errors.Wrap(err, "error retieving XSRF Token")
}
// Grab the web response that has the xsrf in it
xsrfBody, err := ioutil.ReadAll(res.Body)
// Unmarshall the answer and store the token
var x = new(XSRF)
err = json.Unmarshal(xsrfBody, &x)
if err != nil {
log.Fatalf("Error unmarshalling xsrf response! %v", err)
}
// Populate our Auth body for the POST
a.Context = "sso"
a.RedirectTo = re.ReplaceAllString(loginDetails.URL, "")
a.Email = loginDetails.Username
a.Password = loginDetails.Password
authBody, err := json.Marshal(a)
if err != nil {
return samlAssertion, errors.Wrap(err, "failed to build auth request body")
}
// Generate our auth request
req, err := http.NewRequest("POST", authSubmitURL, strings.NewReader(string(authBody)))
if err != nil {
return samlAssertion, errors.Wrap(err, "error building authentication request")
}
// Add the necessary headers to the auth request
req.Header.Add("X-Xsrftoken", x.Token)
req.Header.Add("Accept", "application/json")
req.Header.Add("Content-Type", "application/json")
res, err = jc.client.Do(req)
if err != nil {
return samlAssertion, errors.Wrap(err, "error retrieving login form")
}
// Check if we get a 401. If we did, MFA is required and the OTP was not provided.
// Get the OTP and resubmit.
if res.StatusCode == 401 {
// Get the user's MFA token and re-build the body
a.OTP = loginDetails.MFAToken
if a.OTP == "" {
a.OTP = prompter.StringRequired("MFA Token")
}
authBody, err = json.Marshal(a)
if err != nil {
return samlAssertion, errors.Wrap(err, "error building authentication req body after getting MFA Token")
}
// Re-request with our OTP
req, err = http.NewRequest("POST", authSubmitURL, strings.NewReader(string(authBody)))
if err != nil {
return samlAssertion, errors.Wrap(err, "error building MFA authentication request")
}
// Re-add the necessary headers to our remade auth request
req.Header.Add("X-Xsrftoken", x.Token)
req.Header.Add("Accept", "application/json")
req.Header.Add("Content-Type", "application/json")
// Resubmit
res, err = jc.client.Do(req)
if err != nil {
return samlAssertion, errors.Wrap(err, "error submitting MFA login form")
}
}
// Check if our auth was successful
if res.StatusCode == 200 {
// Grab the body from the response that has the redirect in it.
reDirBody, err := ioutil.ReadAll(res.Body)
// Unmarshall the body to get the redirect address
var jcrd = new(JCRedirect)
err = json.Unmarshal(reDirBody, &jcrd)
if err != nil {
log.Fatalf("Error unmarshalling redirectTo response! %v", err)
}
// Send the final GET for our SAML response
res, err = jc.client.Get(jcrd.Address)
if err != nil {
return samlAssertion, errors.Wrap(err, "error submitting request for SAML value")
}
//try to extract SAMLResponse
doc, err := goquery.NewDocumentFromReader(res.Body)
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
}
})
} else {
errMsg := fmt.Sprintf("error when trying to auth, status code %d", res.StatusCode)
return samlAssertion, errors.Wrap(err, errMsg)
}
return samlAssertion, nil
}