Copied to clipboard

WKWebView redirect with cookies

For one of our native iOS projects, part of the functionality is implemented using a web view. In these web views, the users need to be authenticated, which happens by injecting authentication cookies. But in one specific case, this user authentication information got lost for no clear reason. We set out to find a solution – let’s show you how we did it!

14/11/2019
-
4
min read
WKWebView redirect with cookies

Let’s start by detailing the problem. For the user to be authenticated at the server side, we inject the user’s authentication cookies into the first request. However, we discovered that when the server responds with a 302 redirect, WKWebview does not pass the cookies set by the server to the redirect URL.

In other words, they’re not being incorporated into the redirected request, and as a result, the user would not be authenticated in the webview. This bug would, of course, be a show stopper for the project – luckily we found a workaround.

High-level solution

The most obvious solution would be to handle that first request manually. When it returns with a 302 redirect, we should, yet again, inject those cookies into that request. However, we can use a special delegate method of the URLSession to intercept the redirect before it is triggered. Here we will inject our authentication cookies and return the altered URLRequest. We’ll then pass the response of that redirected request back to the WKWebView. From then on, the webview flow will take its intended flow.

It only seems safe and possible to add this mechanism to the initial load/request. Consecutive load requests would not necessarily be full page loads, so it would prove nearly impossible to inject those responses at the correct point.

You are welcome to add suggestions and/or improvements to our GitHub page.

Now let’s dive into a little more detail...

Setup

For this to work, we need to make a custom subclass of the WKWebView.

{% c-block language="js" %}
class WKCookieWebView : WKWebView {
/* // Custom implementation */
}
{% c-block-end %}


First, we’ll override the load(_:) function. Here we will do the first request manually via a URLSession.


{% c-block language="js" %}
   private func requestWithCookieHandling(_ request: URLRequest, success: @escaping (URLRequest, HTTPURLResponse?, Data?) -> Void, failure: @escaping () -> Void) {
       let sessionConfig = URLSessionConfiguration.default
       let session = URLSession(configuration: sessionConfig, delegate: self, delegateQueue: nil)
       let task = session.dataTask(with: request) { (data, response, error) in
           if let _ = error {
               failure()
           } else {
/*                // Handle success */
       }
       task.resume()
   }
}
{% c-block-end %}

Handling the redirect

The URLSessionTaskDelegate will call a specific function when a redirect will occur. We will use this function to add the cookies from the response to the new request.

{% c-block language="js" %}
extension WKCookieWebView : URLSessionTaskDelegate {
   func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) {
       syncCookies(request) { (newRequest) in
           completionHandler(newRequest)
       }
   }
}
{% c-block-end %}


Adding the cookies

The necessary cookies will be stored in the shared HTTPCookieStorage. There are a number of ways to aggregate these cookies.

Our function syncCookies(_:) will return an enriched URLRequest with these cookies. When we have a URLSessionTask or a valid url from the URLRequest, we will request the cookies set for that task or request. Otherwise we will just request all the cookies present at that moment. The latter is not very performant, but intended as a last resort. In our case, we will always have a task or request, so we can add only the necessary cookies.

When we’ve received the cookies, we then convert them to a request header fields dictionary and add this as a request header. This way the authentication cookies will be present in the new request.

{% c-block language="js" %}
extension WKCookieWebView {
private func syncCookies(_ request: URLRequest, _ task: URLSessionTask? = nil, _ completion: @escaping
(URLRequest) -> Void) {
var request = request
var cookiesArray: [HTTPCookie] = []

       if let task = task {
           HTTPCookieStorage.shared.getCookiesFor(task, completionHandler: { (cookies) in
               if let cookies = cookies {
                   cookiesArray.append(contentsOf: cookies)

                   let cookieDict = HTTPCookie.requestHeaderFields(with: cookiesArray)
                   if let cookieStr = cookieDict["Cookie"] {
                       request.addValue(cookieStr, forHTTPHeaderField: "Cookie")
                   }
               }
               completion(request)
           })
       } else  if let url = request.url {
           if let cookies = HTTPCookieStorage.shared.cookies(for: url) {
               cookiesArray.append(contentsOf: cookies)
           }
           let cookieDict = HTTPCookie.requestHeaderFields(with: cookiesArray)
           if let cookieStr = cookieDict["Cookie"] {
               request.addValue(cookieStr, forHTTPHeaderField: "Cookie")
           }
           completion(request)

       } else {
           if let cookies = HTTPCookieStorage.shared.cookies {
               cookiesArray.append(contentsOf: cookies)
           }
           let cookieDict = HTTPCookie.requestHeaderFields(with: cookiesArray)
           if let cookieStr = cookieDict["Cookie"] {
               request.addValue(cookieStr, forHTTPHeaderField: "Cookie")
           }
           completion(request)
       }
   }
}
{% c-block-end %}


Finishing of the redirect

When the redirected request returns successfully or failed, we pass it through to the WKWebView. From then on, everything is again handled internally by the WKWebView and the system can do its thing!

The full custom subclass can be found on GitHub.

Did you just say, “ah, brilliant, I needed that!” out loud? We can do you one better: we’re growing our team. Hop over to our jobs page, and maybe next time you’ll be on the front row of these solutions…

No items found.

Read further

Written by

Jens Reynders

Jens

Engineer

Being the Swift advocate he has always been, Jens is our point of contact when in need of Apple expertise. He’s a senior in our team, already part of our team for more than 5 years. Jens doesn’t only commute to work by bike, his weekends are often filled with training and participating in triathlons.