diff --git a/.codeclimate.yml b/.codeclimate.yml index cb720230..ab912f1c 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -74,7 +74,7 @@ plugins: gofmt: enabled: true golint: - enabled: true + enabled: false exclude_patterns: - "**/" # exclude all - "!./cmd/" # unexclude just the ones we want to lint diff --git a/.gitignore b/.gitignore index bf7b7190..9d59e4eb 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,8 @@ dev/tmp build_ca_certificate .vscode/ +ci/identity/users.yml + # CLI binaries conjur # Exclude binary entrypoint diff --git a/ci/identity/policy.yml b/ci/identity/policy.yml new file mode 100644 index 00000000..e5271d82 --- /dev/null +++ b/ci/identity/policy.yml @@ -0,0 +1,31 @@ +- !policy + id: conjur + body: + - !policy + id: authn-oidc + body: + - !policy + id: identity + body: + - !webservice + + - !variable provider-uri + - !variable client-id + - !variable client-secret + + # URI of Conjur instance + - !variable redirect_uri + + # Defines the JWT claim to use as the Conjur identifier + - !variable claim-mapping + + # Group with permission to authenticate + - !group + id: authenticatable + annotations: + description: Users who can authenticate using this authenticator + + - !permit + role: !group authenticatable + privilege: [ read, authenticate ] + resource: !webservice diff --git a/ci/identity/secrets.yml b/ci/identity/secrets.yml new file mode 100644 index 00000000..9fcd4352 --- /dev/null +++ b/ci/identity/secrets.yml @@ -0,0 +1,9 @@ +ci: + IDENTITY_CLIENT_ID: !var ci/identity/app/client-id + IDENTITY_CLIENT_SECRET: !var ci/identity/app/client-secret + IDENTITY_PROVIDER_URI: !var ci/identity/app/provider-uri + +development: + IDENTITY_CLIENT_ID: !var dev/identity/app/client-id + IDENTITY_CLIENT_SECRET: !var dev/identity/app/client-secret + IDENTITY_PROVIDER_URI: !var dev/identity/app/provider-uri diff --git a/ci/identity/users.template.yml b/ci/identity/users.template.yml new file mode 100644 index 00000000..3aa6c159 --- /dev/null +++ b/ci/identity/users.template.yml @@ -0,0 +1,7 @@ +# Users with permission to authenticate +- !user {{ IDENTITY_USERNAME }} + +- !grant + members: + - !user {{ IDENTITY_USERNAME }} + role: !group conjur/authn-oidc/identity/authenticatable diff --git a/dev/docker-compose.yml b/dev/docker-compose.yml index 5c8716d6..716bfa95 100644 --- a/dev/docker-compose.yml +++ b/dev/docker-compose.yml @@ -32,6 +32,7 @@ services: environment: - OKTA_USERNAME=${OKTA_USERNAME:-user} - OKTA_PASSWORD=${OKTA_PASSWORD:-password} + - IDENTITY_USERNAME=${IDENTITY_USERNAME:-user} command: bash -c "cd ${PWD}/..; make install; sleep infinity" working_dir: ${PWD}/.. restart: on-failure diff --git a/dev/start b/dev/start index 9406aa71..bf47c2a9 100755 --- a/dev/start +++ b/dev/start @@ -13,8 +13,10 @@ source "../ci/keycloak/keycloak_functions.sh" # TODO: add a prompt and an -f flag to give the user control over destroying an already existing development environment. # It can be such a pain to mistakenly destroy your environment. -ENABLE_AUTHN_OIDC=false +ENABLE_OIDC_KEYCLOAK=false ENABLE_OIDC_OKTA=false +ENABLE_OIDC_IDENTITY=false +export IDENTITY_USERNAME="" # Minimal set of services. We add to this list based on cmd line flags. services=(pg conjur proxy cli-dev) @@ -111,7 +113,7 @@ EOL echo echo "Setting up authenticators" - init_oidc + configure_oidc_providers # Updates CONJUR_AUTHENTICATORS and restarts required services. start_auth_services @@ -127,11 +129,16 @@ EOL docker-compose exec cli-dev bash -c 'conjur logout conjur init --force-netrc --force -u http://conjur -i -a dev -t oidc --service-id okta-2 conjur login -i $OKTA_USERNAME -p $OKTA_PASSWORD' - elif [ "$ENABLE_AUTHN_OIDC" = true ]; then + elif [ "$ENABLE_OIDC_KEYCLOAK" = true ]; then echo "Setting up Conjur for OIDC (Keycloak)" docker-compose exec cli-dev bash -c 'conjur logout conjur init --force-netrc --force -u http://conjur -i -a dev -t oidc --service-id keycloak conjur login -i alice -p alice' + elif [ "$ENABLE_OIDC_IDENTITY" = true ]; then + echo "Setting up Conjur for OIDC (Identity)" + docker-compose exec cli-dev bash -c 'conjur logout +conjur init --force-netrc --force -u http://conjur -i -a dev -t oidc --service-id identity +conjur login -i $IDENTITY_USERNAME' fi echo @@ -141,8 +148,10 @@ conjur login -i alice -p alice' parse_options() { while true ; do case "$1" in - --authn-oidc ) ENABLE_AUTHN_OIDC=true ; shift ;; + --oidc-keycloak ) ENABLE_OIDC_KEYCLOAK=true ; shift ;; --oidc-okta ) ENABLE_OIDC_OKTA=true ; shift ;; + --oidc-identity ) ENABLE_OIDC_IDENTITY=true ; shift ;; + --identity-user ) IDENTITY_USERNAME="$2" ; shift ; shift ;; # -h | --help ) print_help ; shift ;; * ) if [ -z "$1" ]; then @@ -153,6 +162,11 @@ parse_options() { fi ;; esac done + + if [[ $ENABLE_OIDC_IDENTITY = true && -z "$IDENTITY_USERNAME" ]]; then + echo "Flag --oidc-identity must be paired with flag --identity-user. See --help." + exit + fi } start_conjur_server() { @@ -183,93 +197,109 @@ client_add_secret() { configure_oidc_providers() { # Start conjur again, since it is recreating by docker-compose because of # dependency with keycloak + if [[ $ENABLE_OIDC_KEYCLOAK = true ]]; then + echo "Configuring Keycloak provider" + setup_keycloak + fi + + if [[ $ENABLE_OIDC_OKTA = true ]]; then + echo "Configuring Okta provider" + setup_okta + fi + + if [[ $ENABLE_OIDC_IDENTITY = true ]]; then + echo "Configuring Identity provider" + setup_identity + fi + + enable_oidc_authenticators +} + +setup_keycloak() { + services+=(keycloak) + docker-compose up -d --no-deps "${services[@]}" start_conjur_server wait_for_keycloak_server fetch_keycloak_certificate - configure_oidc_authenticators - enable_oidc_authenticators + setup_oidc_client "keycloak" "https://keycloak:8443/auth/realms/master" "conjurClient" "1234" "email" create_keycloak_users echo "keycloak admin console url: http://0.0.0.0:7777/auth/admin" } -configure_oidc_authenticators() { - echo "Setting Keycloak policy and variables values in Conjur" - client_load_policy "../ci/keycloak/policy.yml" - - client_add_secret 'conjur/authn-oidc/keycloak/provider-uri' 'https://keycloak:8443/auth/realms/master' - client_add_secret 'conjur/authn-oidc/keycloak/client-id' 'conjurClient' - client_add_secret 'conjur/authn-oidc/keycloak/client-secret' '1234' - client_add_secret 'conjur/authn-oidc/keycloak/claim-mapping' 'email' - client_add_secret 'conjur/authn-oidc/keycloak/redirect_uri' 'http://127.0.0.1:8888/callback' +setup_okta() { + check_environment_variables "OKTA_PROVIDER_URI" "OKTA_CLIENT_ID" "OKTA_CLIENT_SECRET" + setup_oidc_client "okta-2" "${OKTA_PROVIDER_URI}oauth2/default" \ + "${OKTA_CLIENT_ID}" "${OKTA_CLIENT_SECRET}" "preferred_username" +} - client_load_policy "../ci/keycloak/users.yml" +setup_identity() { + check_environment_variables "IDENTITY_PROVIDER_URI" "IDENTITY_CLIENT_ID" "IDENTITY_CLIENT_SECRET" + generate_identity_policy + setup_oidc_client "identity" "${IDENTITY_PROVIDER_URI}" \ + "${IDENTITY_CLIENT_ID}" "${IDENTITY_CLIENT_SECRET}" "email" +} - if [[ $ENABLE_OIDC_OKTA = true ]]; then - echo "Confuring Okta policy and variables values in Conjur" - client_load_policy "../ci/okta/policy.yml" +setup_oidc_client() { + service_id="$1" + provider_uri="$2" + client_id="$3" + client_secret="$4" + claim_mapping="$5" - local okta_valid=true - if [[ -z "$OKTA_PROVIDER_URI" ]]; then - echo "OKTA_PROVIDER_URI is not set" - okta_valid=false - fi + trim_service_id="${service_id%-*}" + echo "Setting $service_id policy and variable values in Conjur" - if [[ -z "$OKTA_CLIENT_ID" ]]; then - echo "OKTA_CLIENT_ID is not set" - okta_valid=false - fi + client_load_policy "../ci/$trim_service_id/policy.yml" - if [[ -z "$OKTA_CLIENT_SECRET" ]]; then - echo "OKTA_CLIENT_SECRET is not set" - okta_valid=false - fi + client_add_secret "conjur/authn-oidc/$service_id/provider-uri" "$provider_uri" + client_add_secret "conjur/authn-oidc/$service_id/client-id" "$client_id" + client_add_secret "conjur/authn-oidc/$service_id/client-secret" "$client_secret" + client_add_secret "conjur/authn-oidc/$service_id/claim-mapping" "$claim_mapping" + client_add_secret "conjur/authn-oidc/$service_id/redirect_uri" "http://127.0.0.1:8888/callback" - if [[ $okta_valid = true ]]; then - # Replace these okta values with your own if not using Summon - client_add_secret 'conjur/authn-oidc/okta-2/provider-uri' "${OKTA_PROVIDER_URI}oauth2/default" - client_add_secret 'conjur/authn-oidc/okta-2/client-id' "$OKTA_CLIENT_ID" - client_add_secret 'conjur/authn-oidc/okta-2/client-secret' "$OKTA_CLIENT_SECRET" - else - echo "Skipped Okta configuration due to missing environment variables" - fi + client_load_policy "../ci/$trim_service_id/users.yml" +} +function generate_identity_policy() { + echo "Generating policy for AuthnOIDC V2 service 'identity' and user '$IDENTITY_USERNAME'" + policy_dir="../ci/identity" + rm -f "$policy_dir/users.yml" + sed -e "s#{{ IDENTITY_USERNAME }}#$IDENTITY_USERNAME#g" "$policy_dir/users.template.yml" > "$policy_dir/users.yml" +} - client_add_secret 'conjur/authn-oidc/okta-2/claim-mapping' 'preferred_username' - client_add_secret 'conjur/authn-oidc/okta-2/redirect_uri' 'http://127.0.0.1:8888/callback' +check_environment_variables() { + vars=("$@") + local valid=true + for i in "${vars[@]}" + do + if [[ -z "${!i}" ]]; then + echo "${i} is not set" + valid=false + fi + done - client_load_policy "../ci/okta/users.yml" + if [[ $valid = false ]]; then + exit 1 fi } enable_oidc_authenticators() { - echo "Configuring Keycloak as OpenID provider for automatic testing" - # We enable an OIDC authenticator without a service-id to test that it's - # invalid. - enabled_authenticators="$enabled_authenticators,authn-oidc/keycloak" + if [[ $ENABLE_OIDC_KEYCLOAK = true ]]; then + echo "Configuring Keycloak as OpenID provider for automatic testing" + # We enable an OIDC authenticator without a service-id to test that it's + # invalid. + enabled_authenticators="$enabled_authenticators,authn-oidc/keycloak" + fi if [[ $ENABLE_OIDC_OKTA = true ]]; then - echo "Configuring OKTA as OpenID provider for manual testing" + echo "Configuring Okta as OpenID provider for manual testing" enabled_authenticators="$enabled_authenticators,authn-oidc/okta-2" fi -} -init_oidc() { - # # ADFS and OKTA make no sense without OIDC. - # if [[ $ENABLE_AUTHN_OIDC = false && - # ($ENABLE_OIDC_ADFS = true || $ENABLE_OIDC_OKTA = true) ]] - # then - # echo "Error: --oidc-adfs and --oidc-okta both require --authn-oidc" - # exit 1 - # fi - - if [[ $ENABLE_AUTHN_OIDC != true ]]; then - return + if [[ $ENABLE_OIDC_IDENTITY = true ]]; then + echo "Configuring Identity as OpenID provider for manual testing" + enabled_authenticators="$enabled_authenticators,authn-oidc/identity" fi - - services+=(keycloak) - docker-compose up -d --no-deps "${services[@]}" - - configure_oidc_providers } start_auth_services() { diff --git a/go.mod b/go.mod index 47a2ce24..7dd37895 100644 --- a/go.mod +++ b/go.mod @@ -33,6 +33,7 @@ require ( github.com/sirupsen/logrus v1.8.1 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/zalando/go-keyring v0.2.2 // indirect + golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect golang.org/x/sys v0.3.0 // indirect golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect golang.org/x/text v0.4.0 // indirect diff --git a/go.sum b/go.sum index 31d209aa..67d4bee7 100644 --- a/go.sum +++ b/go.sum @@ -63,6 +63,8 @@ github.com/zalando/go-keyring v0.2.2 h1:f0xmpYiSrHtSNAVgwip93Cg8tuF45HJM6rHq/A5R github.com/zalando/go-keyring v0.2.2/go.mod h1:sI3evg9Wvpw3+n4SqplGSJUMwtDeROfD4nsFz4z9PG0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug= +golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= diff --git a/pkg/clients/authn_oidc_dev.go b/pkg/clients/authn_oidc_dev.go index 7259c586..1f48bd4a 100644 --- a/pkg/clients/authn_oidc_dev.go +++ b/pkg/clients/authn_oidc_dev.go @@ -9,6 +9,7 @@ import ( "encoding/json" "errors" "fmt" + "golang.org/x/exp/slices" "html" "io" "io/ioutil" @@ -16,12 +17,20 @@ import ( "net/url" "regexp" "strings" + "time" + + "github.com/cyberark/conjur-cli-go/pkg/prompts" ) // OidcLogin attempts to login to Conjur using the OIDC flow. Username and password can be provided to // bypass the browser and use the username and password to fetch an OIDC code. This option is meant for testing // purposes only and will print a warning. func OidcLogin(conjurClient ConjurClient, username string, password string) (ConjurClient, error) { + username, password, err := prompts.MaybeAskForCredentials(username, password) + if err != nil { + return nil, err + } + // If a username and password is provided, attempt to use them to fetch an OIDC code instead of opening a browser oidcPromptHandler := openBrowser if username != "" && password != "" { @@ -48,6 +57,9 @@ func fetchOidcCodeFromProvider(username, password string) func(providerURL strin case strings.Contains(providerURL, "okta"): fmt.Println("Using Okta as OIDC provider") return fetchOidcCodeFromOkta(httpClient, username, password, providerURL) + case strings.Contains(providerURL, "idaptive"): + fmt.Println("Using Identity as OIDC provider") + return fetchOidcCodeFromIdentity(httpClient, username, password, providerURL) default: fmt.Println("Using Keycloak as OIDC provider") return fetchOidcCodeFromKeycloak(httpClient, username, password, providerURL) @@ -99,7 +111,7 @@ func fetchOidcCodeFromKeycloak(httpClient *http.Client, username, password, prov // If the login was successful, the provider will redirect to the callback URL with a code if resp.StatusCode == 302 { - callbackRedirect(resp) + callbackRedirect(resp.Header.Get("Location")) return nil } @@ -122,7 +134,7 @@ func fetchOidcCodeFromOkta(httpClient *http.Client, username, password, provider // If the login was successful, redirect to the callback URL with a code if resp.StatusCode == 302 { - callbackRedirect(resp) + callbackRedirect(resp.Header.Get("Location")) return nil } return errors.New("unable to fetch authorization code from Okta") @@ -180,8 +192,7 @@ func fetchSessionTokenFromOkta(httpClient *http.Client, providerURL string, user return respJSON.SessionToken, nil } -func callbackRedirect(resp *http.Response) error { - location := string(resp.Header.Get("Location")) +func callbackRedirect(location string) error { go func() { // Send a request to the callback URL to pass the code back to the CLI resp, err := http.Get(location) @@ -194,6 +205,249 @@ func callbackRedirect(resp *http.Response) error { return nil } +func fetchOidcCodeFromIdentity(httpClient *http.Client, username, password, providerURL string) error { + authToken, err := fetchAuthTokenFromIdentity(httpClient, providerURL, username, password) + if err != nil { + return err + } + + target := providerURL + code := 0 + for !strings.Contains(target, "127.0.0.1:8888/callback") { + req, err := http.NewRequest("GET", target, nil) + if err != nil { + return err + } + req.Header.Add("Accept", "*/*") + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", authToken)) + + resp, err := httpClient.Do(req) + if err != nil { + return err + } + + target = resp.Header.Get("Location") + code = resp.StatusCode + } + + if code == 302 { + callbackRedirect(target) + return nil + } + return errors.New("unable to fetch authorization code from Identity") +} + +type startAuthData struct { + Version string `json:"Version"` + Username string `json:"User"` +} + +type advanceAuthData struct { + Action string `json:"Action"` + Answer string `json:"Answer,omitempty"` + MechanismId string `json:"MechanismId"` + SessionId string `json:"SessionId"` +} + +type mechanism struct { + PromptSelectMech string `json:"PromptSelectMech"` + MechanismId string `json:"MechanismId"` +} + +type startAuthResponse struct { + Success bool `json:"success"` + Result struct { + SessionId string `json:"SessionId,omitempty"` + Challenges []struct { + Mechanisms []mechanism `json:"Mechanisms"` + } `json:"Challenges,omitempty"` + Summary string `json:"Summary,omitempty"` + PodFqdn string `json:"PodFqdn",omitempty` + } `json:"Result"` + Message string `json:"Message"` + MessageId string `json:"MessageID"` + Exception string `json:"Exception"` + ErrorId string `json:"ErrorID"` + ErrorCode string `json:"ErrorCode"` + IsSoftError bool `json:"IsSoftError"` + InnerExceptions string `json:"InnerExceptions"` +} + +type advanceAuthResponse struct { + Success bool `json:"success"` + Result struct { + Summary string `json:"Summary"` + GeneratedAuthValue string `json:"GeneratedAuthValue"` + } `json:"Result"` + Message string `json:"Message"` + MessageId string `json:"MessageID"` + Exception string `json:"Exception"` + ErrorId string `json:"ErrorID"` + ErrorCode string `json:"ErrorCode"` + IsSoftError bool `json:"IsSoftError"` + InnerExceptions string `json:"InnerExceptions"` +} + +func authRequest[data startAuthData | advanceAuthData, responseContent startAuthResponse | advanceAuthResponse]( + endpoint string, + payload data, + httpClient *http.Client, +) (*http.Response, responseContent, error) { + var content responseContent + + byteData, _ := json.Marshal(payload) + req, err := http.NewRequest("POST", endpoint, bytes.NewBuffer(byteData)) + if err != nil { + fmt.Println(err) + return nil, content, err + } + req.Header.Add("Accept", "*/*") + req.Header.Add("Content-Type", "application/json") + + resp, err := httpClient.Do(req) + if err != nil { + fmt.Println(err) + return nil, content, err + } + defer resp.Body.Close() + responseBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, content, err + } + + err = json.Unmarshal(responseBody, &content) + if err != nil { + return nil, content, err + } + + return resp, content, nil +} + +func startAuthRequest(hostname string, data startAuthData, httpClient *http.Client) (*http.Response, startAuthResponse, error) { + endpoint := hostname + "/Security/StartAuthentication" + return authRequest[startAuthData, startAuthResponse](endpoint, data, httpClient) +} + +func advanceAuthRequest(hostname string, data advanceAuthData, httpClient *http.Client) (*http.Response, advanceAuthResponse, error) { + endpoint := hostname + "/Security/AdvanceAuthentication" + return authRequest[advanceAuthData, advanceAuthResponse](endpoint, data, httpClient) +} + +func fetchAuthTokenFromIdentity(httpClient *http.Client, providerURL string, username string, password string) (string, error) { + fmt.Println("Attempting to get auth token from Identity using the provided username/password") + + // A request to /Security/StartAuthentication begins the login process, + // and returns a list of authentication mechanisms to engage with. + + host := extractHostname(providerURL) + startData := startAuthData{ + Version: "1.0", + Username: username, + } + _, startResp, err := startAuthRequest(host, startData, httpClient) + if err != nil { + return "", err + } + if startResp.Result.PodFqdn != "" { + host = fmt.Sprintf("https://%s", startResp.Result.PodFqdn) + _, startResp, err = startAuthRequest(host, startData, httpClient) + if err != nil { + return "", err + } + } + + primaryMechanisms := startResp.Result.Challenges[0].Mechanisms + secondaryMechanisms := startResp.Result.Challenges[1].Mechanisms + + // Usually, we would iterate through MFA challenges sequentially. + // For our purposes, though, we want to make sure to use the Password + // and Mobile Authenticator mechanisms. + + passwordMechanism := primaryMechanisms[slices.IndexFunc( + primaryMechanisms, + func(m mechanism) bool { + return m.PromptSelectMech == "Password" + }, + )] + mobileAuthMechanism := secondaryMechanisms[slices.IndexFunc( + secondaryMechanisms, + func(m mechanism) bool { + return m.PromptSelectMech == "Mobile Authenticator" + }, + )] + + // Advance Password-based authentication handshake. + + resp, advanceResp, err := advanceAuthRequest(host, advanceAuthData{ + Action: "Answer", + Answer: password, + MechanismId: passwordMechanism.MechanismId, + SessionId: startResp.Result.SessionId, + }, httpClient) + if err != nil { + return "", nil + } + if !(advanceResp.Success) { + return "", errors.New(advanceResp.Message) + } + + // Advance Mobile Authenticator-based authentication handshake. + + resp, advanceResp, err = advanceAuthRequest(host, advanceAuthData{ + Action: "StartOOB", + MechanismId: mobileAuthMechanism.MechanismId, + SessionId: startResp.Result.SessionId, + }, httpClient) + if err != nil { + return "", err + } + if !(advanceResp.Success) { + return "", errors.New(advanceResp.Message) + } + fmt.Println(fmt.Sprintf("\nDev env users: select %s in your Identity notification", advanceResp.Result.GeneratedAuthValue)) + + // For 30 seconds, Poll for out-of-band authentication success. + + poll := func() error { + ticker := time.NewTicker(3 * time.Second) + defer ticker.Stop() + timeout := time.After(30 * time.Second) + for { + select { + case <-timeout: + return errors.New("Timed out waiting for out-of-band authentication") + case <-ticker.C: + resp, advanceResp, err = advanceAuthRequest(host, advanceAuthData{ + Action: "Poll", + MechanismId: mobileAuthMechanism.MechanismId, + SessionId: startResp.Result.SessionId, + }, httpClient) + if err != nil { + return err + } + + if advanceResp.Result.Summary == "LoginSuccess" { + return nil + } + } + } + } + + err = poll() + if err != nil { + return "", err + } + + // When the OOB Poll response indicates LoginSuccess, the bearer token is + // included as an .ASPXAUTH cookie. + + return resp.Cookies()[slices.IndexFunc( + resp.Cookies(), func(c *http.Cookie) bool { + return c.Name == ".ASPXAUTH" + }, + )].Value, nil +} + func extractHostname(providerURL string) string { url, err := url.Parse(providerURL) if err != nil {