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