Skip to content

Commit 861ee54

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

File tree

3 files changed

+254
-4
lines changed

3 files changed

+254
-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: 251 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@ 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"
1921
)
2022

2123
// 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
4850
case strings.Contains(providerURL, "okta"):
4951
fmt.Println("Using Okta as OIDC provider")
5052
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)
5156
default:
5257
fmt.Println("Using Keycloak as OIDC provider")
5358
return fetchOidcCodeFromKeycloak(httpClient, username, password, providerURL)
@@ -99,7 +104,7 @@ func fetchOidcCodeFromKeycloak(httpClient *http.Client, username, password, prov
99104

100105
// If the login was successful, the provider will redirect to the callback URL with a code
101106
if resp.StatusCode == 302 {
102-
callbackRedirect(resp)
107+
callbackRedirect(resp.Header.Get("Location"))
103108
return nil
104109
}
105110

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

123128
// If the login was successful, redirect to the callback URL with a code
124129
if resp.StatusCode == 302 {
125-
callbackRedirect(resp)
130+
callbackRedirect(resp.Header.Get("Location"))
126131
return nil
127132
}
128133
return errors.New("unable to fetch authorization code from Okta")
@@ -180,8 +185,7 @@ func fetchSessionTokenFromOkta(httpClient *http.Client, providerURL string, user
180185
return respJSON.SessionToken, nil
181186
}
182187

183-
func callbackRedirect(resp *http.Response) error {
184-
location := string(resp.Header.Get("Location"))
188+
func callbackRedirect(location string) error {
185189
go func() {
186190
// Send a request to the callback URL to pass the code back to the CLI
187191
resp, err := http.Get(location)
@@ -194,6 +198,249 @@ func callbackRedirect(resp *http.Response) error {
194198
return nil
195199
}
196200

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

0 commit comments

Comments
 (0)