TheM4hd1 / SwiftyInsta

Instagram Private API Swift
MIT License
225 stars 51 forks source link

Login failure using webview #163

Closed anonrig closed 4 years ago

anonrig commented 4 years ago

I'm receiving the following error message on try? result.get() using instagramcontroller, any idea why @TheM4hd1 ?

"https://i.instagram.com/api/v1/accounts/current_user/.\nInvalid response.\nProcessing handler returned `nil`.\n403"
anonrig commented 4 years ago

This is related to using a useragent, but don't know what the expectation is.

"Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1"

sbertix commented 4 years ago

Endpoints can only be called through an Instagram app user agent.
403 means you're forbidden to access that resource, in this specific case due to this.
Nothing we can fix, not a problem with SwiftyInsta 💪
@anonrig

MariaJsn commented 4 years ago

Hi @anonrig @sbertix Which agent should I use to login successfully? I tried many of them but I get login failed message :( Please can you help me?

Thanks, Maria

sbertix commented 4 years ago

I suggest sticking to the default one, leaving it nil.
Any other valid user agent should work for logging in as long as it's not used for calling the endpoints, like above, cause they require their own custom Instagram app one. @mariajsn

MariaJsn commented 4 years ago

But when I use nil then I see android advertisement and apple will not approve my app like this. @sbertix

sbertix commented 4 years ago

Unfortunately that's the supported user agent because it works, if you manage to find one that works just as well without displaying the ad, let us know, considering the ad cannot be removed through JavaScript injection either. @mariajsn

Beware though that apps built with SwiftyInsta are definitely not App Store safe: you gain control of an entire user's account, no tokens, no authorization, no nothing, which is a big no no for the Review guidelines, other than the Instagram terms and conditions, meaning your app is gonna be rejected at one point, if not at first, because of this. Ad or no ad.

TheM4hd1 commented 4 years ago

use this user agent for webView: Mozilla/5.0 (iPhone; CPU iPhone OS 13_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 and it displays the App Store ads, then use the default user agent for Private API, it works well.

MariaJsn commented 4 years ago

@TheM4hd1 @sbertix I used this user agent but I can login failed again.

My code:

let login = LoginWebViewController(userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 13_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148", completionHandler: { controller, result in

    controller.dismiss(animated: true, completion: nil)

    // deal with authentication response.
    guard let (response, _) = try? result.get() else { return print("Login failed.") }
    print("Login successful.")
    // persist cache safely in the keychain for logging in again in the future.
    guard let key = response.persist() else { return print("`Authentication.Response` could not be persisted.") }

})

override func viewDidLoad() {
    super.viewDidLoad()

        if #available(iOS 13, *) {
            present(login, animated: true, completion: nil) // just swipe down to dismiss.
        } else {
            present(UINavigationController(rootViewController: login),  // already adds a `Cancel` button to dismiss it.
                    animated: true,
                    completion: nil)
        }

}
TheM4hd1 commented 4 years ago

I tried it on iOS 12.0, iPhone 7, Simulator and iOS 13.3, iPhone XS, Real Device both worked well, I don't see a problem. I just tried hard coded user agent in LoginWebView', and left everything asdefault` make sure to clean build the project and build again if you used hard coded changes.

MariaJsn commented 4 years ago

@TheM4hd1 so my code is wrong? also I insert the code inside Loginwebwiew class init method and it shows Google play advertisement. Is it possible can you show me your code which has working one?

public init(frame: CGRect, userAgent: String? = nil, didReachEndOfLoginFlow: (() -> Void)? = nil) {
    // delete all cookies.
    HTTPCookieStorage.shared.removeCookies(since: Date.distantPast)
    // update the process pool.
    let configuration = WKWebViewConfiguration()
    configuration.processPool = WKProcessPool()
    // init login.
    self.userAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 13_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148"//userAgent
    self.didReachEndOfLoginFlow = didReachEndOfLoginFlow
    super.init(frame: frame, configuration: configuration)
    self.navigationDelegate = self
}
TheM4hd1 commented 4 years ago

again, leave everything as default, https://github.com/TheM4hd1/SwiftyInsta/blob/e6c43dbbf2b50354842ebfd53ddb71f46aa40313/SwiftyInsta/UI/LoginWebView.swift#L66 you can hard code the user agent here, leave everything as default, Clean Build the project and build again, no changes applies if you don't clean build. if this worked for you probably there's a bug in initializers and we need to double check the codes. but I tried this and worked without any issue.

sbertix commented 4 years ago

Custom user agents in LoginWebView are fine.
The error above, the 403, is clearly referenced when trying to call the current user, meaning the custom user agent was applied to the APIHandler as well by mistake.
Do you mind printing out your error, @mariajsn? Cause if it's the same that started this issue, the problem is not in LoginWebView.

MariaJsn commented 4 years ago

@TheM4hd1 I changed the code like in LoginWebView line 66 me.customUserAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 13_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148"

cleaned the project and build it again. Tried with Iphone 6s and which has the software version 13.4. I get the login failed error again.

Yes I can see the appstore advertisement it is good.

Open new project and tried it again and still same getting the login failed error.

this is the my viewController

import UIKit import SwiftyInsta

class ViewController: UIViewController {

let login = LoginWebViewController{ controller, result in

     //controller.modalPresentationStyle = .fullScreen
     controller.dismiss(animated: true, completion: nil)

     // deal with authentication response.
     guard let (response, _) = try? result.get() else { return print("Login failed.") }
     //print("Login successful.")
     // persist cache safely in the keychain for logging in again in the future.
     guard let key = response.persist() else { return print("`Authentication.Response` could not be persisted.") }

 }

override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view.

    if #available(iOS 13, *) {
        present(login, animated: true, completion: nil) // just swipe down to dismiss.
    } else {
        present(UINavigationController(rootViewController: login),  // already adds a `Cancel` button to dismiss it.
                animated: true,
                completion: nil)
    }

}

}

sbertix commented 4 years ago

Do you mind printing the login error you get? @MariaJsn Instead of doing

// deal with authentication response.
guard let (response, _) = try? result.get() else { return print("Login failed.") }
//print("Login successful.")
// persist cache safely in the keychain for logging in again in the future.
guard let key = response.persist() else { return print("`Authentication.Response` could not be persisted.") }

Write

switch result {
    case .failure(let error): print(error)
    case .success(let response):
             guard let key = response.0.persist() else { print("No key.") }
             print(key)
}

And paste in here the output of print, please.

MariaJsn commented 4 years ago

@sbertix the error is custom("https://i.instagram.com/api/v1/accounts/current_user/.\nInvalid response.\nProcessing handler returned nil.\n403")

sbertix commented 4 years ago

Yep, so same as above, meaning the issue is not in LoginWebView.
userAgent however is never set to the APIHandler inside LoginWebViewController, and User-Agent is just a header field, not a cookie or something that can be persisted throughout requests, so I'm really struggling to understand what the problem is.

I'll test some more and update you if I find something.

MariaJsn commented 4 years ago

This is my code. If you want you can try with them and maybe you can find the error easily.

Changed the code inside LoginWebView line 66 like:

me.customUserAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 13_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148"

and my viewcontroller code:

import UIKit import SwiftyInsta

class ViewController: UIViewController {

let login = LoginWebViewController{ controller, result in

     //controller.modalPresentationStyle = .fullScreen
     controller.dismiss(animated: true, completion: nil)

     // deal with authentication response.
     guard let (response, _) = try? result.get() else { return print("Login failed.") }

     //print("Login successful.")
     // persist cache safely in the keychain for logging in again in the future.
     guard let key = response.persist() else { return print("`Authentication.Response` could not be persisted.") }

 }

override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view.

    if #available(iOS 13, *) {
        present(login, animated: true, completion: nil) // just swipe down to dismiss.
    } else {
        present(UINavigationController(rootViewController: login),  // already adds a `Cancel` button to dismiss it.
                animated: true,
                completion: nil)
    }

}

}

sbertix commented 4 years ago

Ok… so it's not an issue of the user agent per se. It seems like any custom user configuration causes a checkpoint to the user.

{
    "errorBody" : "Please log back in.",
    "errorTitle" : "You've Been Logged Out",
    "logoutReason" : 3,
    "message" : "login_required",
    "status" : "fail"
}

The cookies are set and everything, but when they're used to fetch anything from a given endpoint the user is forced to log out.
Maybe because Instagram knows we're using iPhone cookies with Android endpoints? Idk.
Genuinely the only solution that comes to mind is reverting support for custom user agents. @TheM4hd1 @MariaJsn

MariaJsn commented 4 years ago

@sbertix @TheM4hd1 if I not wrong understand we do not have any solution for it :( so how @TheM4hd1 run it in his phone? Is it related with the phone performance?

sbertix commented 4 years ago

Cause I feel like he either has that device he was testing, so it's somehow linked to his profile, or he used that user agent before Instagram changed things.

Again though: this is not a problem with LoginWebView, seems more like a problem with how SwiftyInsta deals with cookies, storage and requests in general (I'm not trying to promote anything, especially cause it's in super early stage, but, for instance, it's not a problem with Swiftagram WebViewAuthenticator).
But, due to the way SwiftyInsta was originally ported, and the way I updated it to 2.* trying to keep to @TheM4hd1's amazing work, instead of rewriting it from the ground up, the code is extremely complicated to follow, and it's not an apparent one, unfortunately.

MariaJsn commented 4 years ago

@TheM4hd1 What do you think? Do you have any idea? And is there any solution in your mind?

TheM4hd1 commented 4 years ago

actually I was using an older version of SwiftyInsta, I'm not sure what version it is, because I installed it from development branch long time ago. I updated to latest version and same error happened. I'm at the version that there is no LoginWebViewController and it's only LoginWebView

this is the file, I replaced it with current version ( a few changes requires for error handling ), and it worked well. so @sbertix can check easier now what the issue is ?

//
//  LoginWebView.swift
//  SwiftyInsta
//
//  Created by Stefano Bertagno on 07/19/2019.
//  Copyright © 2019 Mahdi. All rights reserved.
//

#if os(iOS)
import UIKit
import WebKit

// MARK: Views
@available(iOS 11, *)
public class LoginWebView: WKWebView, WKNavigationDelegate {
    /// Called when reaching the end of the login flow.
    ///  You should probably hide the `InstagramLoginWebView` and notify the user with an activity indicator.
    public var didReachEndOfLoginFlow: (() -> Void)?
    /// Called once the flow is completed.
    var completionHandler: ((Result<[HTTPCookie], Error>) -> Void)!

    // MARK: Init
    public init(frame: CGRect,
                configuration: WKWebViewConfiguration = .init(),
                didReachEndOfLoginFlow: (() -> Void)? = nil) {
        // update the process pool.
        let copy = configuration.copy() as? WKWebViewConfiguration ?? WKWebViewConfiguration()
        copy.processPool = WKProcessPool()
        // init login.
        self.didReachEndOfLoginFlow = didReachEndOfLoginFlow
        super.init(frame: frame, configuration: copy)
        self.navigationDelegate = self
    }

    @available(*, unavailable, message: "use `init(frame:configuration:didReachEndOfLoginFlow:didSuccessfullyLogIn:completionHandler:)` instead.")
    private override init(frame: CGRect, configuration: WKWebViewConfiguration) {
        fatalError("init(frame:, configuration:) has been removed")
    }
    @available(*, unavailable)
    public required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    // MARK: Log in
    func authenticate(completionHandler: @escaping (Result<[HTTPCookie], Error>) -> Void) {
        // update completion handler.
        self.completionHandler = completionHandler
        // wipe all cookies and wait to load.
        deleteAllCookies { [weak self] in
            guard let me = self else { return }
            guard let url = URL(string: "https://www.instagram.com/accounts/login/") else {
                return
            }
            // in some iOS versions, use-agent needs to be different.
            // this use-agent works on iOS 11.4 and iOS 12.0+
            // but it won't work on lower versions.
//            me.customUserAgent = ["(Linux; Android 5.0; iPhone Build/LRX21T)",
//                                  "AppleWebKit/537.36 (KHTML, like Gecko)",
//                                  "Chrome/70.0.3538.102 Mobile Safari/537.36"].joined(separator: " ")
            me.customUserAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 13_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148"
            // load request.
            me.load(URLRequest(url: url))
        }
    }

    // MARK: Clean cookies
    private func fetchCookies() {
        configuration.websiteDataStore.httpCookieStore.getAllCookies { [weak self] in
            self?.completionHandler?(.success($0))
        }
    }

    private func deleteAllCookies(completionHandler: @escaping () -> Void) {
        HTTPCookieStorage.shared.removeCookies(since: Date.distantPast)
        WKWebsiteDataStore.default().removeData(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(),
                                                modifiedSince: .distantPast,
                                                completionHandler: completionHandler)
    }

    // MARK: Navigation delegate
    public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        guard webView.url?.absoluteString == "https://www.instagram.com/" else { return }
        // notify user.
        didReachEndOfLoginFlow?()
        // fetch cookies.
        fetchCookies()
    }
}
#endif
sbertix commented 4 years ago

Wasn't this the early 2.0 version we ended up changing because it caused problem when trying to logging in with more than one account? @them4hd1

Either way, either by starting with this snippet or with WebViewAuthenticator in Swiftagram I'll try to push an update solving these issues.
Due to the Error being called on endpoints, though, I'm not confident changing just LoginWebView will make it work consistently: it might help for some cases, but not fix it for good.

TheM4hd1 commented 4 years ago

yes I guess that's the version. error 403 happens because @MariaJsn can't login via webView, after entering user/pass Instagram just returns to home page and ask to login again. so the login process failed and 403 happens. but when I tried with this old one, after entering user/pass, it redirects to my feed (so login succeed) and I can run requests from APIHandler.

sbertix commented 4 years ago

Except 403 does not happen on LoginWebView, but on AuthenticationHandler @them4hd1.

So, like I said, the issue is not related to LoginWebView functionality per se, but probably the changes I had to make related to cookies and persistency to make it work.
I thought it might have to do with LoginWebView finishing too early but that's not the case, even when removing the "fetch cookie attempts", their actual value does not change, eventually.
It looks like it might be related to AuthenticationHandler non receiving the updated cookies, which was not an issue back then cause they were never properly cleared.

tl;dr: again, the 403 does not happen on LoginWebView but in AuthenticationHandler when asking for the current user, suggesting the issue relates to cookies now being scraped too thoroughly, not giving it a chance to load the resource, whereas back then the cookies were never actually deleted properly.

TheM4hd1 commented 4 years ago

that's what I meant, changing userAgent cause the login fail. when a successful login happens on LoginWebView it redirects you to instagram feed page. but here is the problem now when changing userAgent, it doesn't redirects to instagram feed page, it ask you to login again. forget the AuthenticationHandler for a few seconds, LoginWebView can't login if userAgent changes. it's not related to AuthenticationHandler, when a login fails on webView it's obvious that AuthenticationHandler can't send requests (403) I'm saying the issue is on LoginWebView or the way we handling cookies. because when I tried replacing the older version with current one, it worked well.

sbertix commented 4 years ago

Yep, but the problem of the feed not appearing in the new version has nothing to do with it: it's actually a feature, and a good one. If you used a custom user agent in the current version but replaced row 118 of LoginWebView.swift with break, you'd still see the feed, you'd see you actually managed to login correctly! We just don't need to show the feed anymore when the cookies are found, cause it was "ugly" to display the feed for a split second, so I removed it. Again, it's completely unrelated to the problem and again: you would see the feed (try it 😊).
The login actually works for the LoginWebView. The problem is only related to cookies as in "the code related to LoginWebView functionality is right", what doesn't work is the way AuthenticationHandler is no longer able to read the cookies it needs, most likely they're applied after the call is made, and because, unlike the older version of your snippet, they're actually cleared from storage, they can't be accessed without actually being passed to it.
Trust me: I wrote this 😊 @TheM4hd1 And if you don't trust me, just try it yourself: remove row 118 and you'll see the feed and that you log in correctly (cause of course you do, otherwise you wouldn't be able to login into Instagram from that device, which is ludicrous to assume that Instagram would just block people to login from an iPhone 6s, for instance) and with the same cookies.

sbertix commented 4 years ago

actually I was using an older version of SwiftyInsta, I'm not sure what version it is, because I installed it from development branch long time ago. I updated to latest version and same error happened. I'm at the version that there is no LoginWebViewController and it's only LoginWebView

this is the file, I replaced it with current version ( a few changes requires for error handling ), and it worked well. so @sbertix can check easier now what the issue is ? […]

This does not work either. It returns the same exact error as the new version @TheM4hd1.
Apparently it's neither a LoginWebView bug, nor a cookie one. It's a security feature implemented by Instagram a year ago or so judging by what I cam up googling

{ "errorBody" : "Please log back in.", "errorTitle" : "You've Been Logged Out", "logoutReason" : 3, "message" : "login_required", "status" : "fail" }

As of lately, accounts marked as spammer, I'm assuming all of the ones we're trying to log in with, since I'm assuming we're all relying on Instagram Private APIs for our side projects and we're not just writing this as proof of concept, are never prompted the Open the Instagram app on your phone checkpoint anymore, and it simply won't move on: and it's not only account based, but also IP based.
Somehow the default user agent for LoginWebView goes through, but using a different one messes up with SwiftyInsta default user agent and cookies (not for the LoginWebView, which again is fine and actually completes logging in, but the actual requests), while it still works in different implementations, probably because their pattern hasn't been recognised by Instagram yet (e.g. Swiftagram).
You can all test this by using the custom init by explicitly passing as argument the default user agent and you'll see it'll still work, so that it's clear that nothing's wrong with the implementation of that.

We would need to find a new way to channel requests, move forward from the user agent used for them below, and we'd still be very likely to fail.

Instagram 85.0.0.21.100 Android (21/5.0.2; 480dpi; 1080x1776; Sony; C6603; C6603; qcom; ru_RU; 146536611)

tl;dr: nothing we can do. It's Instagram's fault.


"Fixes"


Disclaimer

To anyone reading this: I hate being a "mod", but I had to lock the conversation, cause enough has been said already.
Since the issue is clearly not related to the LoginWebView anymore, please open a new one if you feel the need to investigate this further but please keep in mind there's nothing me or @TheM4hd1 can do to fix this.