func (c *aadClient) checkMfa(ctx context.Context, authRes *aadAuthResponse) error {
	var res *http.Response
	var err error
	if len(authRes.UrlSkipMfaRegistration) > 0 {
		req, _ := newHttpRequest(ctx, http.MethodGet, authRes.UrlSkipMfaRegistration)
		res, err = checkResponseError(c.httpClient.Do(req.Request))
		if err != nil {
			return err
		}
		return parseResponse(res.Body, authRes)
	} else if len(authRes.UserProofs) > 0 {
		res, err = c.handleMfa(authRes)
		if err != nil {
			return err
		}
		return parseResponse(res.Body, authRes)
	}
	return nil
}
nolint:bodyclose // response bodies closed in parseResponse
package external
import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"github.com/PuerkitoBio/goquery"
	"github.com/mmmorris1975/aws-runas/credentials"
	"github.com/mmmorris1975/aws-runas/identity"
	"io"
	"net/http"
	"net/url"
	"regexp"
	"strconv"
	"strings"
	"time"
)
const (
	aadIdentityProvider   = "AzureADIdentityProvider"
	aadBadUserPassErrCode = 50126
	aadInvalidMfaErrCode  = 500121
	aadMfaMethodNotify = "PhoneAppNotification"
	aadMfaMethodOTP    = "PhoneAppOTP"
	aadMfaMethodSMS    = "OneWaySMS"
)
var (
	bodyRe = regexp.MustCompile(`\$Config=({.+?});`)
	samlRe = regexp.MustCompile(`window\.location\s*=\s*'(https.+SAMLRequest=.+?)';`)
)
type aadClient struct {
	*baseClient
	tenantId string
	appId    string
}
func NewAadClient(url string) (*aadClient, error) {
	bc, err := newBaseClient(url)
	if err != nil {
		return nil, err
	}
	c := &aadClient{baseClient: bc}
	if err = c.parseTenantId(); err != nil {
		return nil, err
	}
	if err = c.parseAppId(); err != nil {
		return nil, err
	}
	return c, nil
}
func (c *aadClient) Authenticate() error {
	return c.AuthenticateWithContext(context.Background())
}
func (c *aadClient) AuthenticateWithContext(ctx context.Context) error {
	req, _ := newHttpRequest(ctx, http.MethodGet, c.authUrl.String())
	res, err := checkResponseError(c.httpClient.Do(req.Request))
	if err != nil {
		return err
	}
	defer res.Body.Close()
	body, err := io.ReadAll(res.Body)
	if err != nil {
		return err
	}
	reader := bytes.NewReader(body)
	authRes := new(aadAuthResponse)
	if err = parseResponseNoClose(reader, authRes); err != nil {
		c.Logger.Debugf("parseResponseNoClose() err: %v, attempting to directly submit response", err)
		_, _ = reader.Seek(0, io.SeekStart)
		_, err = c.submitResponse(res.Request.URL, io.NopCloser(reader))
		return err
	}
	if err = c.auth(ctx, authRes.LoginUrl(res.Request.URL), authRes); err != nil {
		return err
	}
	if strings.HasSuffix(authRes.UrlPost, "/kmsi") {
		c.Logger.Debugf("handling kmsi")
		kmsiUrl := authRes.LoginUrl(res.Request.URL)
		kmsiForm := url.Values{}
		kmsiForm.Set(authRes.FTName, authRes.FT)
		kmsiForm.Set("ctx", authRes.Ctx)
		kmsiForm.Set("LoginOptions", "1")
		req, _ = newHttpRequest(ctx, http.MethodPost, kmsiUrl.String())
		res, err = checkResponseError(c.httpClient.Do(req.withValues(kmsiForm).Request))
		if err != nil {
			return err
		}
	}
	_, err = c.submitResponse(res.Request.URL, res.Body)
	return err
}
func (c *aadClient) Identity() (*identity.Identity, error) {
	return c.identity(aadIdentityProvider), nil
}
func (c *aadClient) Roles(...string) (*identity.Roles, error) {
	if c.saml == nil || len(*c.saml) < 1 {
		var err error
		c.saml, err = c.SamlAssertion()
		if err != nil {
			return nil, err
		}
	}
	return c.roles()
}
func (c *aadClient) IdentityToken() (*credentials.OidcIdentityToken, error) {
	return c.IdentityTokenWithContext(context.Background())
}
func (c *aadClient) IdentityTokenWithContext(context.Context) (*credentials.OidcIdentityToken, error) {
	oauthUrlBase := fmt.Sprintf("https://login.microsoftonline.com/%s/oauth2/v2.0", c.tenantId)
	pkce, err := newPkceCode()
	if err != nil {
		return nil, err
	}
	authzQS := c.pkceAuthzRequest(pkce.Challenge())
	var vals url.Values
	vals, err = c.oauthAuthorize(fmt.Sprintf("%s/authorize", oauthUrlBase), authzQS, false)
	if err != nil {
		return nil, err
	}
	if vals.Get("state") != authzQS.Get("state") {
		return nil, errOauthStateMismatch
	}
	token, err := c.oauthToken(fmt.Sprintf("%s/token", oauthUrlBase), vals.Get("code"), pkce.Verifier())
	if err != nil {
		return nil, err
	}
	return token.IdToken, nil
}
func (c *aadClient) SamlAssertion() (*credentials.SamlAssertion, error) {
	return c.SamlAssertionWithContext(context.Background())
}
func (c *aadClient) SamlAssertionWithContext(ctx context.Context) (*credentials.SamlAssertion, error) {
	req, _ := newHttpRequest(ctx, http.MethodGet, c.authUrl.String())
	res, err := checkResponseError(c.httpClient.Do(req.Request))
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()
	data, _ := io.ReadAll(res.Body)
	match := samlRe.FindSubmatch(data)
	if match == nil || len(match) < 2 {
		if err = c.AuthenticateWithContext(ctx); err != nil {
			return nil, err
		}
		return c.SamlAssertionWithContext(ctx)
	}
	u, _ := url.Parse(string(match[1]))
	if err = c.samlRequest(ctx, u); err != nil {
		return nil, err
	}
	return c.saml, nil
}
func (c *aadClient) parseTenantId() error {
	c.tenantId = c.authUrl.Query().Get("tenantId")
	if len(c.tenantId) < 1 {
		return errors.New("tenant ID not found in url")
	}
	return nil
}
func (c *aadClient) parseAppId() error {
	parts := strings.Split(c.authUrl.Path, `/`)
	c.appId = parts[len(parts)-1]
	if len(parts) < 3 || len(c.appId) < 1 {
		return errors.New("app ID not found in url")
	}
	return nil
}
func (c *aadClient) submitResponse(u *url.URL, r io.ReadCloser) (*http.Response, error) {
	defer r.Close()
	doc, err := goquery.NewDocumentFromReader(r)
	if err != nil {
		return nil, err
	}
	form := doc.Find("body form")
	formAction, ok := form.Attr("action")
	if !ok {
		return nil, errors.New("missing form submit url")
	}
	var submitUrl *url.URL
	submitUrl, err = url.Parse(formAction)
	if err != nil {
		return nil, err
	}
	if len(submitUrl.Scheme) < 1 {
		submitUrl = u.ResolveReference(submitUrl)
	}
	vals := url.Values{}
	form.Find("input").Each(func(i int, s *goquery.Selection) {
		var name, val string
		name, ok = s.Attr("name")
		if !ok {
			return
		}
		val, ok = s.Attr("value")
		if !ok {
			return
		}
		vals.Set(name, val)
	})
	req, _ := newHttpRequest(context.Background(), http.MethodPost, submitUrl.String())
	return checkResponseError(c.httpClient.Do(req.withValues(vals).Request))
}
func (c *aadClient) auth(ctx context.Context, authUrl *url.URL, authRes *aadAuthResponse) error {
	authForm := url.Values{}
	authForm.Set(authRes.FTName, authRes.FT)
	authForm.Set("ctx", authRes.Ctx)
	authForm.Set("login", c.Username)
	req, _ := newHttpRequest(ctx, http.MethodPost, authUrl.String())
	res, err := checkResponseError(c.httpClient.Do(req.withValues(authForm).Request))
	if err != nil {
		return err
	}
	if err = parseResponse(res.Body, authRes); err != nil {
		return err
	}
	if u := authRes.CredentialTypeResult.Credentials.FederationRedirectUrl; len(u) > 0 {
		res, err = c.doFederatedAuth(u)
		if err != nil {
			return err
		}
	} else {
		authRes.ErrCode = ""
		if err = c.gatherCredentials(); err != nil {
			return err
		}
		authForm.Set("passwd", c.Password)
		res, err = checkResponseError(c.httpClient.Do(req.withValues(authForm).Request))
		if err != nil {
			return err
		}
	}
	if err = parseResponse(res.Body, authRes); err != nil {
		return err
	} else if authRes.ErrCode == strconv.Itoa(aadBadUserPassErrCode) {
		return errors.New("authentication failed")
	}
	return c.checkMfa(ctx, authRes)
}
func (c *aadClient) doFederatedAuth(fedUrl string) (res *http.Response, err error) {
	cfg := AuthenticationClientConfig{
		CredentialInputProvider: c.CredentialInputProvider,
		MfaTokenProvider:        c.MfaTokenProvider,
		IdentityProviderName:    aadIdentityProvider,
		Logger:                  c.Logger,
		MfaType:                 c.MfaType,
		Username:                c.Username,
		Password:                c.Password,
	}
	if len(c.FederatedUsername) > 0 {
		cfg.Username = c.FederatedUsername
	}
	sc := MustGetSamlClient("", fedUrl, cfg)
	if err = sc.Authenticate(); err != nil {
		return nil, err
	}
	req, _ := newHttpRequest(context.Background(), http.MethodGet, fedUrl)
	res, err = checkResponseError(c.httpClient.Do(req.Request))
	if err != nil {
		return nil, err
	}
	return c.submitResponse(res.Request.URL, res.Body)
}