Home > front end >  How to debug URLSession POST Authentication Login Failure
How to debug URLSession POST Authentication Login Failure

Time:09-10

I am trying to mimic an HTTP Form POST in iOS to login to a server.

When I use Safari or Chrome and submit the login form outside of my app, I can login without issue. I am using Safari and Chrome dev tools to record/review the "correct" request and response headers, cookies and body during the GET and POST required to login.

When I run my app in Xcode, I use debug print statements or Instruments to review the headers, cookies and body.

Is there a tool/method that will allow me to compare my app's GET and POST header and body vs. what a web browser does? I want an "apples to apples" comparison that will allow me to determine what I am doing wrong...

My code is below. The POST header returns status code = 419. The post body includes the text "Page Expired", which leads me to believe I am not handling tokens or cookies correctly.

Code overview:

  1. I press a UI button to invoke login(). This does a GET of login page, and saves the hidden _token form input from the response body. Cookies are saved to cookieStorage.

  2. I press a UI button to invoke loginPost(). This submits a form with a bogus email and password. I format headers and body. I expect to get an error indicating email is not registered. POST adds _token to body. This body seems to match Chrome dev tools for urlencode formatting. Status code 419 is returned..

Code

class LoginAPI {
  public let avLogin = "https://someDomain.com/login"

  // save response, data from last getHTMLPage() GET
  fileprivate var lastGetResponse: HTTPURLResponse? = nil
  fileprivate var lastGetData: String? = nil

  // test POST with saved values
  var loginToken = ""
  var cookies: [HTTPCookie] = []

  // MARK: Login
  func login() async -> String {
    // GET login page,
    let loginGetHTML = await self.getHTMLPage(url: self.avLogin)

    let loginToken = self.scrapeLoginToken(html: loginGetHTML)
    let cookies = self.getCookiesFromResponse(response: self.lastGetResponse)
    if let lastResponse = self.lastGetResponse,
       let lastURL = lastResponse.url {

      HTTPCookieStorage.shared.setCookies(cookies,
        for: lastURL, mainDocumentURL: nil)
    }

    // allow testing of Login, then POST
    self.loginToken = loginToken
    self.cookies = cookies

  // TO DO: add delay, then call loginPost(), and return Data as String
    return ""
  }

  // MARK: POST Login form
  func loginPost(url: String, loginToken: String, cookies: [HTTPCookie]) async {
    guard let loginURL = URL(string: url) else {return}

    let email = "[email protected]"
    let password = "pass123"

    var request = URLRequest(url: loginURL)
    request.httpMethod = "POST"
    request.url = loginURL

    // header
    request.httpShouldHandleCookies = true

    // body
    let loginInfo = [
      ("_token"  , loginToken),
      ("email"   , email),
      ("password", password)
    ]
    let body = urlEncode(loginInfo)
    request.httpBody = Data(body.utf8)

    let session = URLSession.shared
    session.configuration.httpCookieStorage = HTTPCookieStorage.shared
    session.configuration.httpCookieAcceptPolicy = .always
    session.configuration.httpShouldSetCookies = true

    let task = session.dataTask(with: request) { (data, response, error) in

      if let error = error {
        print ("POST error: \(error)")
      }

      guard let response = response as? HTTPURLResponse else {
        print("invalid POST response")
        return
      }

      print("response")
      let statusCode = response.statusCode
      let headerFields = response.allHeaderFields
      let cookies = headerFields["Set-Cookie"]
      // let cookie = response.value(forKey: "Set-Cookie")
      print("  status code = \(statusCode)")
      print("  cookies = \(cookies.debugDescription)")
      print(response)

      if let mimeType = response.mimeType,
         let data = data,
         let page = String(data: data, encoding: .utf8) {
        print("mimeType \(mimeType)")
        print("page as UTF-8")
        print(page)
      }
    }
    task.resume()
  }

  // MARK: GET
  public func getHTMLPage(url urlString: String) async -> String {
    var statusCode = 0        // HTTP Response status code

    // void prior cached response, data
    self.lastGetResponse = nil
    self.lastGetData = nil

    guard let url = URL(string: urlString) else {
      print("Error: Invalid URL: '\(urlString)'")
      return ""
    }

    do {
      let (data, response) = try await URLSession.shared.data(from: url)

      if let httpResponse = response as? HTTPURLResponse {
        statusCode = httpResponse.statusCode
        self.lastGetResponse = httpResponse
        print("GET response")
        print(response)
      } else {
        print("Error: couldn't get HTTP Response")
        return ""
      }

      guard statusCode == 200 else {
        print("Error: Bad HTTP status code.  code=\(statusCode)")
        return ""
      }

      let page = String(decoding: data, as: UTF8.self)
      self.lastGetData = page
      return page

    } catch {
      print("Error: catch triggerred")
      return ""
    }
  }

  // MARK: Login Helper Functions
  private func getCookiesFromResponse(response: HTTPURLResponse?) -> [HTTPCookie] {

    guard let response = response,
          let responseURL = response.url else {
      return []
    }

    guard let responseHeaderFields = response.allHeaderFields as? [String : String] else {
      return []
    }

    let cookies = HTTPCookie.cookies(
      withResponseHeaderFields: responseHeaderFields,
      for: responseURL)
    return cookies
  }

  // MARK: Login token
  public func scrapeLoginToken(html: String) -> String {
    look for name="_token", value="40-char-string"
    return <40-char-string
  }

  // MARK: Login urlEncode
  public func urlEncode(_ params: [(String, String)]) -> String {
    var paramArray: [String] = []
    for param in params {
      let (name, value) = param
      let valueEnc = urlEncode(value)
      paramArray.append("\(name)=\(valueEnc)")
    }
    let body = paramArray.joined(separator: "&")
    return body
  }

  private func urlEncode(_ string: String) -> String {
    let allowedCharacters = CharacterSet.alphanumerics
    return string.addingPercentEncoding(
      withAllowedCharacters: allowedCharacters) ?? ""
  }   
}

Any debug help or direction would be appreciated!

CodePudding user response:

I was able to resolve this issue with Proxyman, a Mac app that places itself between your mac and the internet, and saves all activity from apps, like Safari and Chrome, as well as apps being developed in Xcode.

Proxyman allowed me to make a simple "apples to apples" comparison, showing me all internet activity made by a browser, my app, or any other app running on my mac, in a common easy to read format.

I am not a web developer, so I didn't realize how easy it would be to debug URLSession headers, cookies, responses, etc. using a proxy server app.

My real problem: I had a simple typo in the web form I was sending to the server, so the server wasn't receiving all necessary form fields.

Days of debug looking into caches, cookies, headers, url encoding, responses, etc. I just didn't see the typo!

  • Related