Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions pkg/igconnector/login_native.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ func (m *MetaNativeLogin) SubmitUserInput(
"Instagram did not accept that code. Enter a new code and try again.",
), nil
}
m.Cancel()
return nil, fmt.Errorf("failed to complete Instagram web two-factor login: %w", err)
}
m.webTwoFactor = nil
Expand All @@ -211,7 +212,10 @@ func (m *MetaNativeLogin) SubmitUserInput(
if isClientHTTPError(err) {
m.User.Log.Warn().Err(err).Msg("Instagram web login request failed on the client")
return m.start(ctx, "The request did not complete on this device. Please try again.")
} else if isMissingInstagramWebTwoFactorCSRF(err) {
return m.start(ctx, "Instagram did not return the security state needed to continue. Please try again.")
}
m.Cancel()
return nil, fmt.Errorf("failed to create Instagram web session: %w", err)
}
if challenge != nil {
Expand Down Expand Up @@ -266,6 +270,10 @@ func isClientHTTPError(err error) bool {
return err != nil && strings.Contains(err.Error(), "error from client: ")
}

func isMissingInstagramWebTwoFactorCSRF(err error) bool {
return err != nil && strings.Contains(err.Error(), "instagram web two-factor challenge is missing a CSRF token")
}

func instagramCredentialsStep(instructions string) *bridgev2.LoginStep {
return &bridgev2.LoginStep{
Type: bridgev2.LoginStepTypeUserInput,
Expand Down
10 changes: 5 additions & 5 deletions pkg/igconnector/login_native_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,17 @@ func (*nativeLoginRoundTripper) RoundTrip(*http.Request) (*http.Response, error)
return nil, nil
}

func TestInstagramLoginFlowsExposeNativeFirstAndKeepCookies(t *testing.T) {
func TestInstagramLoginFlowsExposeCookiesFirstAndKeepNative(t *testing.T) {
connector := &IGConnector{}
flows := connector.GetLoginFlows()
if len(flows) != 2 {
t.Fatalf("expected two login flows, got %d", len(flows))
}
if flows[0].ID != FlowIDInstagramPassword {
t.Fatalf("expected native flow first, got %q", flows[0].ID)
if flows[0].ID != FlowIDInstagramCookies {
t.Fatalf("expected cookie flow first, got %q", flows[0].ID)
}
if flows[1].ID != FlowIDInstagramCookies {
t.Fatalf("expected cookie fallback second, got %q", flows[1].ID)
if flows[1].ID != FlowIDInstagramPassword {
t.Fatalf("expected native flow second, got %q", flows[1].ID)
}
process, err := connector.CreateLogin(
context.Background(),
Expand Down
87 changes: 82 additions & 5 deletions pkg/instameow/login_web.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ const instagramWebTwoFactorValidateCodeDocID = "26264014419868193"

var ErrInstagramWebTwoFactorCodeRejected = errors.New("instagram web two-factor code was rejected")

var errInstagramWebTwoFactorMissingCSRF = errors.New("instagram web two-factor challenge is missing a CSRF token")

type InstagramWebTwoFactorChallenge struct {
TOTP bool
SMS bool
Expand All @@ -62,6 +64,7 @@ type instagramWebTwoFactorState struct {
encryptedContext string
maskedContactPoint string
method string
csrfToken string
}

type instagramWebLoginResponse struct {
Expand Down Expand Up @@ -268,6 +271,8 @@ func (c *Client) logInstagramWebRequestRejection(
func (c *Client) captureInstagramWebTwoFactor(
result instagramWebLoginResponse,
fallbackUsername string,
preResponseCSRFToken string,
statusCode int,
) (*InstagramWebTwoFactorChallenge, error) {
info := result.TwoFactorInfo
username := info.Username
Expand All @@ -293,13 +298,31 @@ func (c *Client) captureInstagramWebTwoFactor(
if method == "SMS" || method == "WHATSAPP" {
maskedContactPoint = info.MaskedPhoneNumber
}
responseCSRFToken := c.cookies.Get(cookies.IGCookieCSRFToken)
csrfToken := responseCSRFToken
retainedCSRFToken := false
if csrfToken == "" {
csrfToken = preResponseCSRFToken
retainedCSRFToken = csrfToken != ""
}
if csrfToken == "" {
return nil, errInstagramWebTwoFactorMissingCSRF
}
c.webTwoFactor = &instagramWebTwoFactorState{
identifier: info.Identifier,
username: username,
encryptedContext: info.EncryptedContext,
maskedContactPoint: maskedContactPoint,
method: method,
}
csrfToken: csrfToken,
}
c.log.Debug().
Int("status_code", statusCode).
Str("challenge_type", method).
Bool("csrf_pre_response_present", preResponseCSRFToken != "").
Bool("csrf_rotated", responseCSRFToken != "" && responseCSRFToken != preResponseCSRFToken).
Bool("csrf_retained", retainedCSRFToken).
Msg("Captured Instagram web two-factor challenge")
return &InstagramWebTwoFactorChallenge{
TOTP: info.TOTP,
SMS: info.SMS,
Expand Down Expand Up @@ -365,6 +388,7 @@ func (c *Client) CreateInstagramWebSession(
if err != nil {
return nil, err
}
preResponseCSRFToken := c.cookies.Get(cookies.IGCookieCSRFToken)
headers := c.http.BuildHeaders(true, false)
headers.Set("origin", baseURL)
headers.Set("referer", loginPageURL)
Expand Down Expand Up @@ -399,7 +423,11 @@ func (c *Client) CreateInstagramWebSession(
instagramWebLoginResponseClass(result),
)
if parseErr == nil && result.TwoFactorRequired {
return c.captureInstagramWebTwoFactor(result, identifier)
statusCode := 0
if response != nil {
statusCode = response.StatusCode
}
return c.captureInstagramWebTwoFactor(result, identifier, preResponseCSRFToken, statusCode)
} else if parseErr == nil && result.Message != "" {
return nil, fmt.Errorf("instagram web login failed: %s", result.Message)
}
Expand All @@ -409,13 +437,15 @@ func (c *Client) CreateInstagramWebSession(
} else if parseErr != nil {
return nil, fmt.Errorf("failed to parse Instagram web login: %w", parseErr)
} else if result.TwoFactorRequired {
return c.captureInstagramWebTwoFactor(result, identifier)
return c.captureInstagramWebTwoFactor(result, identifier, preResponseCSRFToken, response.StatusCode)
} else if !result.Authenticated {
if result.Message != "" {
return nil, fmt.Errorf("instagram web login failed: %s", result.Message)
}
return nil, errors.New("instagram web login did not authenticate")
} else if missing := c.cookies.GetMissingCookieNames(); len(missing) > 0 {
}
c.ensureInstagramWebUserID()
if missing := c.cookies.GetMissingCookieNames(); len(missing) > 0 {
return nil, fmt.Errorf("instagram web login succeeded without required cookies: %v", missing)
}
return nil, nil
Expand Down Expand Up @@ -523,6 +553,9 @@ func (c *Client) completeInstagramWebTwoFactorLegacy(
)
if response != nil {
c.cookies.UpdateFromResponse(response)
if csrfToken := c.cookies.Get(cookies.IGCookieCSRFToken); csrfToken != "" {
state.csrfToken = csrfToken
}
}
var result instagramWebLoginResponse
parseErr := json.Unmarshal(body, &result)
Expand Down Expand Up @@ -579,6 +612,9 @@ func (c *Client) completeInstagramWebTwoFactorEncrypted(
)
if response != nil {
c.cookies.UpdateFromResponse(response)
if csrfToken := c.cookies.Get(cookies.IGCookieCSRFToken); csrfToken != "" {
state.csrfToken = csrfToken
}
}
body = bytes.TrimPrefix(body, httpclient.AntiJSPrefix)
var result instagramWebTwoFactorGraphQLResponse
Expand Down Expand Up @@ -624,6 +660,16 @@ func (c *Client) CompleteInstagramWebSessionTwoFactor(
return errors.New("instagram web two-factor code is empty")
}
state := c.webTwoFactor
if state.csrfToken == "" {
return errInstagramWebTwoFactorMissingCSRF
}
c.cookies.Set(cookies.IGCookieCSRFToken, state.csrfToken)
headers := c.http.BuildHeaders(true, false)
c.log.Debug().
Bool("csrf_cookie_present", c.cookies.Get(cookies.IGCookieCSRFToken) != "").
Bool("cookie_header_present", headers.Get("cookie") != "").
Bool("csrf_header_present", headers.Get("x-csrftoken") != "").
Msg("Prepared Instagram web two-factor CSRF state")
var err error
if state.encryptedContext == "" {
err = c.completeInstagramWebTwoFactorLegacy(ctx, state, verificationCode)
Expand All @@ -632,9 +678,40 @@ func (c *Client) CompleteInstagramWebSessionTwoFactor(
}
if err != nil {
return err
} else if missing := c.cookies.GetMissingCookieNames(); len(missing) > 0 {
}
c.ensureInstagramWebUserID()
if missing := c.cookies.GetMissingCookieNames(); len(missing) > 0 {
return fmt.Errorf("instagram web two-factor login succeeded without required cookies: %v", missing)
}
c.webTwoFactor = nil
return nil
}

// ensureInstagramWebUserID derives the ds_user_id cookie from the sessionid when
// Instagram authenticates without returning it as its own cookie, which happens
// on the encrypted two-factor GraphQL path.
func (c *Client) ensureInstagramWebUserID() {
if c.cookies.Get(cookies.IGCookieDSUserID) != "" {
return
}
if userID := instagramWebUserIDFromSessionID(c.cookies.Get(cookies.IGCookieSessionID)); userID != "" {
c.cookies.Set(cookies.IGCookieDSUserID, userID)
}
}

// instagramWebUserIDFromSessionID extracts the numeric account ID from the
// sessionid cookie, whose value is "<ds_user_id>:<token>:<...>" (percent-encoded).
func instagramWebUserIDFromSessionID(sessionID string) string {
if sessionID == "" {
return ""
}
decoded, err := url.QueryUnescape(sessionID)
if err != nil {
decoded = sessionID
}
userID, _, _ := strings.Cut(decoded, ":")
if _, err := strconv.ParseInt(userID, 10, 64); err != nil {
return ""
}
return userID
}
4 changes: 2 additions & 2 deletions pkg/messagix/cookies/cookies.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ const (
)

var FBRequiredCookies = []MetaCookieName{FBCookieXS, FBCookieCUser, MetaCookieDatr}
var IGRequiredCookies = []MetaCookieName{IGCookieSessionID, IGCookieCSRFToken, IGCookieDSUserID, IGCookieMachineID, IGCookieDeviceID}
var IGOptionalCookies = []MetaCookieName{IGCookieRUR, IGCookieSHBID, IGCookieSHBTS}
var IGRequiredCookies = []MetaCookieName{IGCookieSessionID, IGCookieCSRFToken, IGCookieDSUserID}
var IGOptionalCookies = []MetaCookieName{IGCookieRUR, IGCookieSHBID, IGCookieSHBTS, IGCookieMachineID, IGCookieDeviceID}

type Cookies struct {
Platform types.Platform
Expand Down
Loading