Debugging ASWebAuthenticationSession

Bonus: attach Safari Web Inspector before critical logic executes

In this blog post, you'll learn how to debug ASWebAuthenticationSession and I'll share a trick on how to buy enough time to attach the Safari Web Inspector before logic executes.

You use ASWebAuthenticationSession to authenticate a user through a web service. When the user navigates to the site’s authentication URL, the site presents the user with a form to collect credentials. After validating the credentials, the site redirects the user’s browser, typically using a custom scheme, to a URL that indicates the outcome of the authentication attempt.

// Use the URL and callback scheme specified by the authorization provider.
guard let authURL = URL(string: "https://example.com/auth") else { return }
let scheme = "exampleauth"

// Initialize the session.
let session = ASWebAuthenticationSession(url: authURL, callbackURLScheme: scheme)
{ callbackURL, error in
    // Handle the callback.
}

ASWebAuthenticationSession runs in a separate process and the content executed is HTML & JavaScript. Hence you can use the Safari Web Inspector to debug the HTML and JavaScript sources. The session is presented in a modal view controller that is similar in appearance to SFSafariViewController, but it is not Safari itself.

As a prerequisite, you have to enable Safari Web Inspector on your Mac

as well as enabling it on your iOS device.

Once ASWebAuthenticationSession presents the web content you can attach to the Safari Web Inspector.

I often run into the tricky situation that the web content executes critical code before I can manually attach the Safari Web Inspector.

I came up with the following trick:

  1. Replace the URL to be launched in ASWebAuthenticationSession with a different URL. The original URL gets encoded and passed as a URL query parameter.

  2. Once the website is loaded I have all the time in the world to attach the Safari Web Inspector.

  3. I manually trigger the navigation to the original URL through a button in the presented website of the ASWebAuthenticationSession.

All details are outlined below but frankly, there is an easier solution. Kudos to Markus Bösch for pointing out that you can a Web Proxy (e.g. Proxyman, Charles, or Fiddler) and set a breakpoint to pause, attach the Safari Web Inspector, and resume.

The approach from Markus is extremely nice as it does not require to re-compile the app!

Back to my original approach: I have deployed the following HTML/JS code as a GitHub page of a GitHub repository of mine.

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width, initial-scale=1">
  </head>
  <body>
    <h1>Redirect</h1>
    <button onclick="redirect()">Redirect</button>
    <h2 id="demo"></h2>
    <script>
      function redirect() {
          const queryString = window.location.search;
          console.log(queryString);
          const urlParams = new URLSearchParams(queryString);
          const orig = urlParams.get('org')
          console.log(orig);
          var originalUrl = decodeURIComponent((orig).replace(/\+/g, '%20'));
          console.log(originalUrl);
          window.location.href = originalUrl;
      }
    </script>
  </body>
</html>

Replacing the URL is fairly easy if you use ASWebAuthenticationSession directly. But what if you use it indirectly, i.e. use a library that determines the original URL and passes it to ASWebAuthenticationSession?

You can use Method Swizzling to replace the initializer of ASWebAuthenticationSession. This works fine for a debug build but for some reason, this does not work for a release build.


import AuthenticationServices

extension ASWebAuthenticationSession {
    static func useDelayRelay() {
        ASWebAuthenticationSession.swizzleTheInit()
    }
}

extension ASWebAuthenticationSession {
    @objc static func swizzleTheInit() {
        let oldSelector = #selector(ASWebAuthenticationSession.init(url:callbackURLScheme:completionHandler:))
        let newSelector = #selector(ASWebAuthenticationSession.swizzledInit(url:callbackURLScheme:completionHandler:))
        let oldMethod = class_getInstanceMethod(self, oldSelector)!
        let newMethod = class_getInstanceMethod(self, newSelector)!
        let oldImplementation = method_getImplementation(oldMethod)
        let newImplementation = method_getImplementation(newMethod)
        _ = method_setImplementation(oldMethod, newImplementation)
        _ = method_setImplementation(newMethod, oldImplementation)
    }

    @objc private func swizzledInit(url: URL, callbackURLScheme: String?, completionHandler: @escaping ASWebAuthenticationSession.CompletionHandler) {
        let encoded = url.absoluteURL.absoluteString.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)

        let queryItems: [URLQueryItem] = [.init(name: "org", value: encoded)]
        var urlComps = URLComponents(string: "https://marcoeidinger.github.io/DelayedRelay/index.html")!
        urlComps.queryItems = queryItems
        let new = urlComps.url!

        // calling the original implementation
        // note: does not work in release build configuration
        self.swizzledInit(url: new, callbackURLScheme: callbackURLScheme, completionHandler: completionHandler)
    }
}

In this video I am illustrating all steps:

Did you find this article valuable?

Support Marco Eidinger by becoming a sponsor. Any amount is appreciated!