Skip to content

Commit d3c6ac6

Browse files
committed
Add Identity login automation for dev env
1 parent e891321 commit d3c6ac6

File tree

3 files changed

+261
-4
lines changed

3 files changed

+261
-4
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ require (
3333
github.com/sirupsen/logrus v1.8.1 // indirect
3434
github.com/spf13/pflag v1.0.5 // indirect
3535
github.com/zalando/go-keyring v0.2.2 // indirect
36+
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect
3637
golang.org/x/sys v0.3.0 // indirect
3738
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
3839
golang.org/x/text v0.4.0 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ github.com/zalando/go-keyring v0.2.2 h1:f0xmpYiSrHtSNAVgwip93Cg8tuF45HJM6rHq/A5R
6363
github.com/zalando/go-keyring v0.2.2/go.mod h1:sI3evg9Wvpw3+n4SqplGSJUMwtDeROfD4nsFz4z9PG0=
6464
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
6565
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
66+
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug=
67+
golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
6668
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
6769
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
6870
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=

pkg/clients/authn_oidc_dev.go

Lines changed: 258 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,28 @@ import (
99
"encoding/json"
1010
"errors"
1111
"fmt"
12+
"golang.org/x/exp/slices"
1213
"html"
1314
"io"
1415
"io/ioutil"
1516
"net/http"
1617
"net/url"
1718
"regexp"
1819
"strings"
20+
"time"
21+
22+
"github.com/cyberark/conjur-cli-go/pkg/prompts"
1923
)
2024

2125
// OidcLogin attempts to login to Conjur using the OIDC flow. Username and password can be provided to
2226
// bypass the browser and use the username and password to fetch an OIDC code. This option is meant for testing
2327
// purposes only and will print a warning.
2428
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+
2534
// If a username and password is provided, attempt to use them to fetch an OIDC code instead of opening a browser
2635
oidcPromptHandler := openBrowser
2736
if username != "" && password != "" {
@@ -48,6 +57,9 @@ func fetchOidcCodeFromProvider(username, password string) func(providerURL strin
4857
case strings.Contains(providerURL, "okta"):
4958
fmt.Println("Using Okta as OIDC provider")
5059
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)
5163
default:
5264
fmt.Println("Using Keycloak as OIDC provider")
5365
return fetchOidcCodeFromKeycloak(httpClient, username, password, providerURL)
@@ -99,7 +111,7 @@ func fetchOidcCodeFromKeycloak(httpClient *http.Client, username, password, prov
99111

100112
// If the login was successful, the provider will redirect to the callback URL with a code
101113
if resp.StatusCode == 302 {
102-
callbackRedirect(resp)
114+
callbackRedirect(resp.Header.Get("Location"))
103115
return nil
104116
}
105117

@@ -122,7 +134,7 @@ func fetchOidcCodeFromOkta(httpClient *http.Client, username, password, provider
122134

123135
// If the login was successful, redirect to the callback URL with a code
124136
if resp.StatusCode == 302 {
125-
callbackRedirect(resp)
137+
callbackRedirect(resp.Header.Get("Location"))
126138
return nil
127139
}
128140
return errors.New("unable to fetch authorization code from Okta")
@@ -180,8 +192,7 @@ func fetchSessionTokenFromOkta(httpClient *http.Client, providerURL string, user
180192
return respJSON.SessionToken, nil
181193
}
182194

183-
func callbackRedirect(resp *http.Response) error {
184-
location := string(resp.Header.Get("Location"))
195+
func callbackRedirect(location string) error {
185196
go func() {
186197
// Send a request to the callback URL to pass the code back to the CLI
187198
resp, err := http.Get(location)
@@ -194,6 +205,249 @@ func callbackRedirect(resp *http.Response) error {
194205
return nil
195206
}
196207

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("\nDev 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+
197451
func extractHostname(providerURL string) string {
198452
url, err := url.Parse(providerURL)
199453
if err != nil {

0 commit comments

Comments
 (0)