51

There's a load of questions which ask this: Can I get UIWebView to view a self signed HTTPS website?

And the answers always involve either:

  1. Use the private api call for NSURLRequest: allowsAnyHTTPSCertificateForHost
  2. Use NSURLConnection instead and the delegate canAuthenticateAgainstProtectionSpace etc

For me, these won't do.
(1) - means I can't submit to the app store successfully.
(2) - using NSURLConnection means the CSS, images and other things that have to be fetched from the server after receiving the initial HTML page do not load.

Does anyone know how to use UIWebView to view a self-signed https webpage please, which does not involve the two methods above?

Or - If using NSURLConnection can in fact be used to render a webpage complete with CSS, images and everything else - that would be great!

Cheers,
Stretch.

Stretch
  • 3,669
  • 2
  • 28
  • 40
  • http://stackoverflow.com/questions/16783416/uiwebview-the-certificate-for-this-server-is-invalid Dest url is `http://web.hnair.net`how can i fixed. – Eric May 28 '13 at 03:56
  • Why use SSL in production code when the certificate in question is invalid - I seem to miss the point. – Till May 31 '13 at 00:05
  • 6
    Enterprise software containing an SSL web interface is deployed at a customer site on an internal network - it is not possible to get a certificate for these websites, they have to be self signed. – Stretch May 31 '13 at 05:14

9 Answers9

76

Finally I got it!

What you can do is this:

Initiate your request using UIWebView as normal. Then - in webView:shouldStartLoadWithRequest - we reply NO, and instead start an NSURLConnection with the same request.

Using NSURLConnection, you can communicate with a self-signed server, as we have the ability to control the authentication through the extra delegate methods which are not available to a UIWebView. So using connection:didReceiveAuthenticationChallenge we can authenticate against the self signed server.

Then, in connection:didReceiveData, we cancel the NSURLConnection request, and start the same request again using UIWebView - which will work now, because we've already got through the server authentication :)

Here are the relevant code snippets below.

Note: Instance variables you will see are of the following type:
UIWebView *_web
NSURLConnection *_urlConnection
NSURLRequest *_request

(I use an instance var for _request as in my case it's a POST with lots of login details, but you could change to use the request passed in as arguments to the methods if you needed.)

#pragma mark - Webview delegate

// Note: This method is particularly important. As the server is using a self signed certificate,
// we cannot use just UIWebView - as it doesn't allow for using self-certs. Instead, we stop the
// request in this method below, create an NSURLConnection (which can allow self-certs via the delegate methods
// which UIWebView does not have), authenticate using NSURLConnection, then use another UIWebView to complete
// the loading and viewing of the page. See connection:didReceiveAuthenticationChallenge to see how this works.
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;
{
    NSLog(@"Did start loading: %@ auth:%d", [[request URL] absoluteString], _authenticated);

    if (!_authenticated) {
        _authenticated = NO;

        _urlConnection = [[NSURLConnection alloc] initWithRequest:_request delegate:self];

        [_urlConnection start];

        return NO;
    }

    return YES;
}


#pragma mark - NURLConnection delegate

- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;
{
    NSLog(@"WebController Got auth challange via NSURLConnection");

    if ([challenge previousFailureCount] == 0)
    {
        _authenticated = YES;

        NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];

        [challenge.sender useCredential:credential forAuthenticationChallenge:challenge];

    } else
    {
        [[challenge sender] cancelAuthenticationChallenge:challenge];
    }
}

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;
{
    NSLog(@"WebController received response via NSURLConnection");

    // remake a webview call now that authentication has passed ok.
    _authenticated = YES;
    [_web loadRequest:_request];

    // Cancel the URL connection otherwise we double up (webview + url connection, same url = no good!)
    [_urlConnection cancel];
}

// We use this method is to accept an untrusted site which unfortunately we need to do, as our PVM servers are self signed.
- (BOOL)connection:(NSURLConnection *)connection canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace *)protectionSpace
{
    return [protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust];
}

I hope this helps others with the same issue I was having!

Stretch
  • 3,669
  • 2
  • 28
  • 40
  • 1
    Nice one, +1. But what about the other resources (css, javascript fetching data)? – lawicko Jul 26 '12 at 09:02
  • ah - well, that's the beauty of `UIWebView` - it does all that for you. The `NSURLConnection` won't automatically fetch from embedded links etc - however here it is only used now to get past the authentication (then we cancel the `NSURLConnection`), then a new `UIWebViewRequest` is created to handle everything else :) – Stretch Jul 26 '12 at 22:44
  • How is _authenticated declared in the .h? – ZeroSkittles Oct 16 '12 at 21:16
  • it's an instance variable: `BOOL _authenticated;` – Stretch Oct 17 '12 at 03:34
  • How do I get the http response for the final loaded URL in UIWebview? didReceiveResponse doesn't necessarily give the response of the final URL. – yogsma Mar 20 '15 at 13:07
  • 1
    Other answers might be more up to date, but voting this up for excellent explanation. – zambrey May 22 '15 at 23:43
  • 1
    @Kevrone see my answer below – spirographer Oct 08 '15 at 01:22
  • Is there possibility to intercept that the error is SSL, @Stretch, and show alert and if user want to continue to make the same implementation? I tried your code but when I get to `didFailedLoadWithError` I can not invoke `didReceiveAuthenticationChallenge`. – new2ios Feb 14 '17 at 10:39
65

Stretch's answer appears to be a great workaround, but it uses deprecated APIs. So, I thought it might be worthy of an upgrade to the code.

For this code sample, I added the routines to the ViewController which contains my UIWebView. I made my UIViewController a UIWebViewDelegate and a NSURLConnectionDataDelegate. Then I added 2 data members: _Authenticated and _FailedRequest. With that, the code looks like this:

-(BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
    BOOL result = _Authenticated;
    if (!_Authenticated) {
        _FailedRequest = request;
        [[NSURLConnection alloc] initWithRequest:request delegate:self];
    }
    return result;
}

-(void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
        NSURL* baseURL = [_FailedRequest URL];
        if ([challenge.protectionSpace.host isEqualToString:baseURL.host]) {
            NSLog(@"trusting connection to host %@", challenge.protectionSpace.host);
            [challenge.sender useCredential:[NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust] forAuthenticationChallenge:challenge];
        } else
            NSLog(@"Not trusting connection to host %@", challenge.protectionSpace.host);
    }
    [challenge.sender continueWithoutCredentialForAuthenticationChallenge:challenge];
}

-(void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)pResponse {
    _Authenticated = YES;
    [connection cancel];
    [_WebView loadRequest:_FailedRequest];
}

I set _Authenticated to NO when I load the view and don't reset it. This seems to allow the UIWebView to make multiple requests to the same site. I did not try switching sites and trying to come back. That may cause the need for resetting _Authenticated. Also, if you are switching sites, you should keep a dictionary (one entry for each host) for _Authenticated instead of a BOOL.

GeneCode
  • 7,545
  • 8
  • 50
  • 85
Prof Von Lemongargle
  • 3,658
  • 31
  • 29
  • 2
    Theres a typo in your code baseURL should be: [_FailedRequest URL] – João Nunes Mar 16 '13 at 11:54
  • I probably should have mentioned that _BaseRequest contains the root URL for the sites I am willing to trust. If I used the URL from _FailedRequest, I would trust everything. For my purpose, I only want to trust a specific host. – Prof Von Lemongargle Mar 20 '13 at 17:51
  • 4
    This works but sometimes it just fails. It's pretty weird. All those delegate method is called just like when it works, but I got "The certificate for this server is invalid. You might be connecting to a server that is pretending to be “mysite.com” which could put your confidential information at risk." I used NSURLConnection `+sendAsynchronousRequest:queue:completionHandler:` method. – Hlung Sep 05 '13 at 07:49
  • These methods are working for me on iOS 8, however the page loading times are taking 10-15 seconds! whereas without ssl it loads pretty quickly. – Darren Sep 24 '14 at 11:32
  • 1
    @Darren, if you have a way to monitor the network traffic, look for a CRL download. There is a good chance that it is checking for a revoked certificate and the download is timing out. – Prof Von Lemongargle Sep 28 '14 at 04:09
  • It doesnt work in iphone 6 and iphone 6 plus.. Is there anything else we need to do exclusively for iphone 6 and iphone6 plus ? It works fine in iPhone 5 ios 8. – nOOb iOS Feb 05 '15 at 12:23
  • The code only initializes a connection without starting it. Can somebody tell me why it still works? – Luong Huy Duc Jul 31 '15 at 10:11
  • 1
    @LuongHuyDuc initWithRequest docs: Returns an initialized URL connection and begins to load the data for the URL request. This is equivalent to calling initWithRequest:delegate:startImmediately: and passing YES for startImmediately. – earnshavian Aug 11 '15 at 15:47
  • Two problems with this code: 1. You should be checking the certificate to see if it makes sense (e.g. compare it to a known-trusted key or insert your own root cert into the set of trusted roots and then do a normal cert check). 2. You should be using default handling, not continuing without credentials. Otherwise, you break authentication. For details, see https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/NetworkingTopics/Articles/OverridingSSLChainValidationCorrectly.html – dgatwood Nov 28 '17 at 17:55
17

This is the Panacea!


BOOL _Authenticated;
NSURLRequest *_FailedRequest;

#pragma UIWebViewDelegate

-(BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request   navigationType:(UIWebViewNavigationType)navigationType {
    BOOL result = _Authenticated;
    if (!_Authenticated) {
        _FailedRequest = request;
        NSURLConnection *urlConnection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
        [urlConnection start];
    }
    return result;
}

#pragma NSURLConnectionDelegate

-(void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
        NSURL* baseURL = [NSURL URLWithString:@"your url"];
        if ([challenge.protectionSpace.host isEqualToString:baseURL.host]) {
            NSLog(@"trusting connection to host %@", challenge.protectionSpace.host);
            [challenge.sender useCredential:[NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust] forAuthenticationChallenge:challenge];
        } else
            NSLog(@"Not trusting connection to host %@", challenge.protectionSpace.host);
    }
    [challenge.sender continueWithoutCredentialForAuthenticationChallenge:challenge];
}

-(void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)pResponse {
_Authenticated = YES;
    [connection cancel];
    [self.webView loadRequest:_FailedRequest];
}

- (void)viewDidLoad{
   [super viewDidLoad];

    NSURL *url = [NSURL URLWithString:@"your url"];
    NSURLRequest *requestURL = [NSURLRequest requestWithURL:url];
    [self.webView loadRequest:requestURL];

// Do any additional setup after loading the view.
}
phemt.latd
  • 1,775
  • 21
  • 33
Wilson Aguiar
  • 211
  • 3
  • 3
  • 2
    You should change your `NSURL* baseURL = [NSURL URLWithString:@"your url"];` to `NSURL* baseURL = [_FailedRequest URL];` in `willSendRequestForAuthenticationChallenge` to allow any url at all, not just the original url that was loaded. – Brad Parks Jun 26 '15 at 12:47
  • @Wilson Aguiar does this code can deploy to **App Store**? thanks – dianyi Apr 19 '17 at 01:10
7

If you want to access a private server with a self-signed certificate just for testing you don't have to write code. You can manually do a system-wide import of the certificate.

To do this, you need to download the server certificate with mobile safari, which then prompts for an import.

This would be usable under the following circumstances:

  • the number of test devices is small
  • you're trusting the certificate of the server

If you don't have access to the server certificate, you can fallback to the following method for extracting it from any HTTPS-server (at least on Linux/Mac, windows guys will have to download an OpenSSL binary somewhere):

echo "" | openssl s_client -connect $server:$port -prexit 2>/dev/null | sed -n -e '/BEGIN\ CERTIFICATE/,/END\ CERTIFICATE/ p' >server.pem

Note, that depending on the OpenSSL version, the certificate may be doubled in the file, so best have a look at it with a text editor. Put the file somewhere on the network or use the

python -m SimpleHTTPServer 8000

shortcut to access it from your mobile safari at http://$your_device_ip:8000/server.pem.

Community
  • 1
  • 1
zliw
  • 976
  • 10
  • 10
4

This is a clever workaround. However, a possibly better (although more code intensive) solution would be to use an NSURLProtocol as demonstrated in Apple's CustomHTTPProtocol sample code. From the README:

"CustomHTTPProtocol shows how to use an NSURLProtocol subclass to intercept the NSURLConnections made by a high-level subsystem that does not otherwise expose its network connections. In this specific case, it intercepts the HTTPS requests made by a web view and overrides server trust evaluation, allowing you to browse a site whose certificate is not trusted by default."

Checkout the full example: https://developer.apple.com/library/ios/samplecode/CustomHTTPProtocol/Introduction/Intro.html

Alex
  • 41
  • 1
3

This is a swift 2.0 compatible equivalent that works for me. I have not converted this code to use NSURLSession instead of NSURLConnection, and suspect that it would add a lot of complexity to get it right.

var authRequest : NSURLRequest? = nil
var authenticated = false
var trustedDomains = [:] // set up as necessary

func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
    if !authenticated {
        authRequest = request
        let urlConnection: NSURLConnection = NSURLConnection(request: request, delegate: self)!
        urlConnection.start()
        return false
    }
    else if isWebContent(request.URL!) { // write your method for this
        return true
    }
    return processData(request) // write your method for this
}

func connection(connection: NSURLConnection, willSendRequestForAuthenticationChallenge challenge: NSURLAuthenticationChallenge) {
    if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
        let challengeHost = challenge.protectionSpace.host
        if let _ = trustedDomains[challengeHost] {
            challenge.sender!.useCredential(NSURLCredential(forTrust: challenge.protectionSpace.serverTrust!), forAuthenticationChallenge: challenge)
        }
    }
    challenge.sender!.continueWithoutCredentialForAuthenticationChallenge(challenge)
}

func connection(connection: NSURLConnection, didReceiveResponse response: NSURLResponse) {
    authenticated = true
    connection.cancel()
    webview!.loadRequest(authRequest!)
}
spirographer
  • 630
  • 4
  • 18
  • still not getting data every time prints "NSURLConnection finished with error - code -1202 , TIC SSL Trust Error [30:0x1c036c600]: 3:0 , NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9813) , Task .<0> HTTP load failed (error code: -1202 – krishan kumar Nov 10 '18 at 05:45
2

Here the working code of swift 2.0

var authRequest : NSURLRequest? = nil
var authenticated = false


func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
                if !authenticated {
                    authRequest = request
                    let urlConnection: NSURLConnection = NSURLConnection(request: request, delegate: self)!
                    urlConnection.start()
                    return false
                }
                return true
}

func connection(connection: NSURLConnection, didReceiveResponse response: NSURLResponse) {
                authenticated = true
                connection.cancel()
                webView!.loadRequest(authRequest!)
}

func connection(connection: NSURLConnection, willSendRequestForAuthenticationChallenge challenge: NSURLAuthenticationChallenge) {

                let host = "www.example.com"

                if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust &&
                    challenge.protectionSpace.host == host {
                    let credential = NSURLCredential(forTrust: challenge.protectionSpace.serverTrust!)
                    challenge.sender!.useCredential(credential, forAuthenticationChallenge: challenge)
                } else {
                    challenge.sender!.performDefaultHandlingForAuthenticationChallenge!(challenge)
                }
}
Velu Loganathan
  • 241
  • 2
  • 11
1

To build off of @spirographer's answer, I put something together for a Swift 2.0 use case with NSURLSession. However, this is still NOT working. See more below.

func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
    let result = _Authenticated
    if !result {
        let sessionConfiguration = NSURLSessionConfiguration.defaultSessionConfiguration()
        let session = NSURLSession(configuration: sessionConfiguration, delegate: self, delegateQueue: NSOperationQueue.mainQueue())
        let task = session.dataTaskWithRequest(request) {
            (data, response, error) -> Void in
            if error == nil {
                if (!self._Authenticated) {
                    self._Authenticated = true;
                    let pageData = NSString(data: data!, encoding: NSUTF8StringEncoding)
                    self.webView.loadHTMLString(pageData as! String, baseURL: request.URL!)

                } else {
                    self.webView.loadRequest(request)
                }
            }
        }
        task.resume()
        return false
    }
    return result
}

func URLSession(session: NSURLSession, didReceiveChallenge challenge: NSURLAuthenticationChallenge, completionHandler: (NSURLSessionAuthChallengeDisposition, NSURLCredential?) -> Void) {
    completionHandler(NSURLSessionAuthChallengeDisposition.UseCredential, NSURLCredential(forTrust: challenge.protectionSpace.serverTrust!))
}

I will get back the initial HTML response, so the page renders the plain HTML, but there is no CSS styles applied to it (seems like the request to get CSS is denied). I see a bunch of these errors:

NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9813)

It seems like any request made with webView.loadRequest is done not within the session, which is why the connection is rejected. I do have Allow Arbitrary Loads set in Info.plist. What confuses me is why NSURLConnection would work (seemingly the same idea), but not NSURLSession.

Community
  • 1
  • 1
Tri Nguyen
  • 9,950
  • 8
  • 40
  • 72
0

First thing UIWebView is deprecated

use WKWebView instead (available from iOS8)

set webView.navigationDelegate = self

implement

extension ViewController: WKNavigationDelegate {

func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
    let trust = challenge.protectionSpace.serverTrust!
    let exceptions = SecTrustCopyExceptions(trust)
    SecTrustSetExceptions(trust, exceptions)
        completionHandler(.useCredential, URLCredential(trust: trust))
    }

}

And add this in plist with domains you want to allow

<key>NSAppTransportSecurity</key>
<dict>
    <key>NSExceptionDomains</key>
    <dict>
        <key>localhost</key>
        <dict>
            <key>NSTemporaryExceptionAllowsInsecureHTTPSLoads</key>
            <false/>
            <key>NSIncludesSubdomains</key>
            <true/>
            <key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>
            <true/>
            <key>NSTemporaryExceptionMinimumTLSVersion</key>
            <string>1.0</string>
            <key>NSTemporaryExceptionRequiresForwardSecrecy</key>
            <false/>
        </dict>
    </dict>
</dict>
Yatheesha
  • 10,412
  • 5
  • 42
  • 45