WKWebView redirect with cookies

profile picture

Jens Engineer

20 Jul 2018  ·  4 min read


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!

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.

class WKCookieWebView : WKWebView {
	// Custom implementation 
}

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

class WKCookieWebView : WKWebView {
    override func load(_ request: URLRequest) -> WKNavigation? {
        requestWithCookieHandling(request, success: { (newRequest , response, data) in
            // Load data
        }, failure: {
            // let WKWebView handle the network error
        })

        return nil
    }


    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()
    }
}

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.

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)
        }
    }
}

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.

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)
        }
    }
}

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…