@@ -9,19 +9,28 @@ import (
9
9
"encoding/json"
10
10
"errors"
11
11
"fmt"
12
+ "golang.org/x/exp/slices"
12
13
"html"
13
14
"io"
14
15
"io/ioutil"
15
16
"net/http"
16
17
"net/url"
17
18
"regexp"
18
19
"strings"
20
+ "time"
21
+
22
+ "github.com/cyberark/conjur-cli-go/pkg/prompts"
19
23
)
20
24
21
25
// OidcLogin attempts to login to Conjur using the OIDC flow. Username and password can be provided to
22
26
// bypass the browser and use the username and password to fetch an OIDC code. This option is meant for testing
23
27
// purposes only and will print a warning.
24
28
func OidcLogin (conjurClient ConjurClient , username string , password string ) (ConjurClient , error ) {
29
+ username , password , err := prompts .MaybeAskForCredentials (username , password )
30
+ if err != nil {
31
+ return nil , err
32
+ }
33
+
25
34
// If a username and password is provided, attempt to use them to fetch an OIDC code instead of opening a browser
26
35
oidcPromptHandler := openBrowser
27
36
if username != "" && password != "" {
@@ -48,6 +57,9 @@ func fetchOidcCodeFromProvider(username, password string) func(providerURL strin
48
57
case strings .Contains (providerURL , "okta" ):
49
58
fmt .Println ("Using Okta as OIDC provider" )
50
59
return fetchOidcCodeFromOkta (httpClient , username , password , providerURL )
60
+ case strings .Contains (providerURL , "idaptive" ):
61
+ fmt .Println ("Using Identity as OIDC provider" )
62
+ return fetchOidcCodeFromIdentity (httpClient , username , password , providerURL )
51
63
default :
52
64
fmt .Println ("Using Keycloak as OIDC provider" )
53
65
return fetchOidcCodeFromKeycloak (httpClient , username , password , providerURL )
@@ -99,7 +111,7 @@ func fetchOidcCodeFromKeycloak(httpClient *http.Client, username, password, prov
99
111
100
112
// If the login was successful, the provider will redirect to the callback URL with a code
101
113
if resp .StatusCode == 302 {
102
- callbackRedirect (resp )
114
+ callbackRedirect (resp . Header . Get ( "Location" ) )
103
115
return nil
104
116
}
105
117
@@ -122,7 +134,7 @@ func fetchOidcCodeFromOkta(httpClient *http.Client, username, password, provider
122
134
123
135
// If the login was successful, redirect to the callback URL with a code
124
136
if resp .StatusCode == 302 {
125
- callbackRedirect (resp )
137
+ callbackRedirect (resp . Header . Get ( "Location" ) )
126
138
return nil
127
139
}
128
140
return errors .New ("unable to fetch authorization code from Okta" )
@@ -180,8 +192,7 @@ func fetchSessionTokenFromOkta(httpClient *http.Client, providerURL string, user
180
192
return respJSON .SessionToken , nil
181
193
}
182
194
183
- func callbackRedirect (resp * http.Response ) error {
184
- location := string (resp .Header .Get ("Location" ))
195
+ func callbackRedirect (location string ) error {
185
196
go func () {
186
197
// Send a request to the callback URL to pass the code back to the CLI
187
198
resp , err := http .Get (location )
@@ -194,6 +205,249 @@ func callbackRedirect(resp *http.Response) error {
194
205
return nil
195
206
}
196
207
208
+ func fetchOidcCodeFromIdentity (httpClient * http.Client , username , password , providerURL string ) error {
209
+ authToken , err := fetchAuthTokenFromIdentity (httpClient , providerURL , username , password )
210
+ if err != nil {
211
+ return err
212
+ }
213
+
214
+ target := providerURL
215
+ code := 0
216
+ for ! strings .Contains (target , "127.0.0.1:8888/callback" ) {
217
+ req , err := http .NewRequest ("GET" , target , nil )
218
+ if err != nil {
219
+ return err
220
+ }
221
+ req .Header .Add ("Accept" , "*/*" )
222
+ req .Header .Add ("Authorization" , fmt .Sprintf ("Bearer %s" , authToken ))
223
+
224
+ resp , err := httpClient .Do (req )
225
+ if err != nil {
226
+ return err
227
+ }
228
+
229
+ target = resp .Header .Get ("Location" )
230
+ code = resp .StatusCode
231
+ }
232
+
233
+ if code == 302 {
234
+ callbackRedirect (target )
235
+ return nil
236
+ }
237
+ return errors .New ("unable to fetch authorization code from Identity" )
238
+ }
239
+
240
+ type startAuthData struct {
241
+ Version string `json:"Version"`
242
+ Username string `json:"User"`
243
+ }
244
+
245
+ type advanceAuthData struct {
246
+ Action string `json:"Action"`
247
+ Answer string `json:"Answer,omitempty"`
248
+ MechanismId string `json:"MechanismId"`
249
+ SessionId string `json:"SessionId"`
250
+ }
251
+
252
+ type mechanism struct {
253
+ PromptSelectMech string `json:"PromptSelectMech"`
254
+ MechanismId string `json:"MechanismId"`
255
+ }
256
+
257
+ type startAuthResponse struct {
258
+ Success bool `json:"success"`
259
+ Result struct {
260
+ SessionId string `json:"SessionId,omitempty"`
261
+ Challenges []struct {
262
+ Mechanisms []mechanism `json:"Mechanisms"`
263
+ } `json:"Challenges,omitempty"`
264
+ Summary string `json:"Summary,omitempty"`
265
+ PodFqdn string `json:"PodFqdn",omitempty`
266
+ } `json:"Result"`
267
+ Message string `json:"Message"`
268
+ MessageId string `json:"MessageID"`
269
+ Exception string `json:"Exception"`
270
+ ErrorId string `json:"ErrorID"`
271
+ ErrorCode string `json:"ErrorCode"`
272
+ IsSoftError bool `json:"IsSoftError"`
273
+ InnerExceptions string `json:"InnerExceptions"`
274
+ }
275
+
276
+ type advanceAuthResponse struct {
277
+ Success bool `json:"success"`
278
+ Result struct {
279
+ Summary string `json:"Summary"`
280
+ GeneratedAuthValue string `json:"GeneratedAuthValue"`
281
+ } `json:"Result"`
282
+ Message string `json:"Message"`
283
+ MessageId string `json:"MessageID"`
284
+ Exception string `json:"Exception"`
285
+ ErrorId string `json:"ErrorID"`
286
+ ErrorCode string `json:"ErrorCode"`
287
+ IsSoftError bool `json:"IsSoftError"`
288
+ InnerExceptions string `json:"InnerExceptions"`
289
+ }
290
+
291
+ func authRequest [data startAuthData | advanceAuthData , responseContent startAuthResponse | advanceAuthResponse ](
292
+ endpoint string ,
293
+ payload data ,
294
+ httpClient * http.Client ,
295
+ ) (* http.Response , responseContent , error ) {
296
+ var content responseContent
297
+
298
+ byteData , _ := json .Marshal (payload )
299
+ req , err := http .NewRequest ("POST" , endpoint , bytes .NewBuffer (byteData ))
300
+ if err != nil {
301
+ fmt .Println (err )
302
+ return nil , content , err
303
+ }
304
+ req .Header .Add ("Accept" , "*/*" )
305
+ req .Header .Add ("Content-Type" , "application/json" )
306
+
307
+ resp , err := httpClient .Do (req )
308
+ if err != nil {
309
+ fmt .Println (err )
310
+ return nil , content , err
311
+ }
312
+ defer resp .Body .Close ()
313
+ responseBody , err := ioutil .ReadAll (resp .Body )
314
+ if err != nil {
315
+ return nil , content , err
316
+ }
317
+
318
+ err = json .Unmarshal (responseBody , & content )
319
+ if err != nil {
320
+ return nil , content , err
321
+ }
322
+
323
+ return resp , content , nil
324
+ }
325
+
326
+ func startAuthRequest (hostname string , data startAuthData , httpClient * http.Client ) (* http.Response , startAuthResponse , error ) {
327
+ endpoint := hostname + "/Security/StartAuthentication"
328
+ return authRequest [startAuthData , startAuthResponse ](endpoint , data , httpClient )
329
+ }
330
+
331
+ func advanceAuthRequest (hostname string , data advanceAuthData , httpClient * http.Client ) (* http.Response , advanceAuthResponse , error ) {
332
+ endpoint := hostname + "/Security/AdvanceAuthentication"
333
+ return authRequest [advanceAuthData , advanceAuthResponse ](endpoint , data , httpClient )
334
+ }
335
+
336
+ func fetchAuthTokenFromIdentity (httpClient * http.Client , providerURL string , username string , password string ) (string , error ) {
337
+ fmt .Println ("Attempting to get auth token from Identity using the provided username/password" )
338
+
339
+ // A request to /Security/StartAuthentication begins the login process,
340
+ // and returns a list of authentication mechanisms to engage with.
341
+
342
+ host := extractHostname (providerURL )
343
+ startData := startAuthData {
344
+ Version : "1.0" ,
345
+ Username : username ,
346
+ }
347
+ _ , startResp , err := startAuthRequest (host , startData , httpClient )
348
+ if err != nil {
349
+ return "" , err
350
+ }
351
+ if startResp .Result .PodFqdn != "" {
352
+ host = fmt .Sprintf ("https://%s" , startResp .Result .PodFqdn )
353
+ _ , startResp , err = startAuthRequest (host , startData , httpClient )
354
+ if err != nil {
355
+ return "" , err
356
+ }
357
+ }
358
+
359
+ primaryMechanisms := startResp .Result .Challenges [0 ].Mechanisms
360
+ secondaryMechanisms := startResp .Result .Challenges [1 ].Mechanisms
361
+
362
+ // Usually, we would iterate through MFA challenges sequentially.
363
+ // For our purposes, though, we want to make sure to use the Password
364
+ // and Mobile Authenticator mechanisms.
365
+
366
+ passwordMechanism := primaryMechanisms [slices .IndexFunc (
367
+ primaryMechanisms ,
368
+ func (m mechanism ) bool {
369
+ return m .PromptSelectMech == "Password"
370
+ },
371
+ )]
372
+ mobileAuthMechanism := secondaryMechanisms [slices .IndexFunc (
373
+ secondaryMechanisms ,
374
+ func (m mechanism ) bool {
375
+ return m .PromptSelectMech == "Mobile Authenticator"
376
+ },
377
+ )]
378
+
379
+ // Advance Password-based authentication handshake.
380
+
381
+ resp , advanceResp , err := advanceAuthRequest (host , advanceAuthData {
382
+ Action : "Answer" ,
383
+ Answer : password ,
384
+ MechanismId : passwordMechanism .MechanismId ,
385
+ SessionId : startResp .Result .SessionId ,
386
+ }, httpClient )
387
+ if err != nil {
388
+ return "" , nil
389
+ }
390
+ if ! (advanceResp .Success ) {
391
+ return "" , errors .New (advanceResp .Message )
392
+ }
393
+
394
+ // Advance Mobile Authenticator-based authentication handshake.
395
+
396
+ resp , advanceResp , err = advanceAuthRequest (host , advanceAuthData {
397
+ Action : "StartOOB" ,
398
+ MechanismId : mobileAuthMechanism .MechanismId ,
399
+ SessionId : startResp .Result .SessionId ,
400
+ }, httpClient )
401
+ if err != nil {
402
+ return "" , err
403
+ }
404
+ if ! (advanceResp .Success ) {
405
+ return "" , errors .New (advanceResp .Message )
406
+ }
407
+ fmt .Println (fmt .Sprintf ("\n Dev env users: select %s in your Identity notification" , advanceResp .Result .GeneratedAuthValue ))
408
+
409
+ // For 30 seconds, Poll for out-of-band authentication success.
410
+
411
+ poll := func () error {
412
+ ticker := time .NewTicker (3 * time .Second )
413
+ defer ticker .Stop ()
414
+ timeout := time .After (30 * time .Second )
415
+ for {
416
+ select {
417
+ case <- timeout :
418
+ return errors .New ("Timed out waiting for out-of-band authentication" )
419
+ case <- ticker .C :
420
+ resp , advanceResp , err = advanceAuthRequest (host , advanceAuthData {
421
+ Action : "Poll" ,
422
+ MechanismId : mobileAuthMechanism .MechanismId ,
423
+ SessionId : startResp .Result .SessionId ,
424
+ }, httpClient )
425
+ if err != nil {
426
+ return err
427
+ }
428
+
429
+ if advanceResp .Result .Summary == "LoginSuccess" {
430
+ return nil
431
+ }
432
+ }
433
+ }
434
+ }
435
+
436
+ err = poll ()
437
+ if err != nil {
438
+ return "" , err
439
+ }
440
+
441
+ // When the OOB Poll response indicates LoginSuccess, the bearer token is
442
+ // included as an .ASPXAUTH cookie.
443
+
444
+ return resp .Cookies ()[slices .IndexFunc (
445
+ resp .Cookies (), func (c * http.Cookie ) bool {
446
+ return c .Name == ".ASPXAUTH"
447
+ },
448
+ )].Value , nil
449
+ }
450
+
197
451
func extractHostname (providerURL string ) string {
198
452
url , err := url .Parse (providerURL )
199
453
if err != nil {
0 commit comments