objective-cssllocalhosttrustself-signed-certificate

How to get IOS app to trust localhost for development API


Using Visual Studio for Mac, I've created a RESFUL API (developed from API project template), which out of the box outputs a very simple JSON file (i.e. ["value1","value2"]) at the following url: https://localhost:5001/api/values.

So, while running the API on Visual Studio in the background, I'm also running XCode on the same computer; in that I'm developing an app which needs to connect to the API url and output the expected JSON response.

The problem is it continually fails due to a trust error: "TIC SSL Trust Error...NSURLSession/NSURLConnection HTTP Load failed".

From what I've researched, I believe I need to install the self-signed certificate of localhost onto my XCode emulator in order for it to permit the app to trust the url. How do I get access to this localhost certificate?

Or is this even the right way to go?

Code that generates the trust error:

// Specify the base url...
static NSString *const API_URL = @"https://localhost:5001";
// Specify completed url ...
NSString *urlAppend = [NSString stringWithFormat:@"/api/values"];
NSString *urlStr = [[[NSString alloc] initWithString:API_URL]
stringByAppendingString:urlAppend];        

// Specify the URL object that can return JSON, XML, etc....
NSURL *url = [[NSURL alloc] initWithString:urlStr];

// Create an NSURLSessionDataTask that does the GET request at the specified URL (using NSURLSession shared session)...
NSURLSessionDataTask *task = [[NSURLSession sharedSession]
              dataTaskWithURL:url
              completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {

                  // Handle response (output respnose to log window))...
                  NSLog(@"%@", [output initWithData:data  encoding:(NSASCIIStringEncoding)] ? : @"no data");
              }];

// Start task ....
[task resume];

Solution

  • The best way to go about this is to Perform Manual Server Trust Authentication for the url of the localhost development server (running in the background). This is done by utilizing the URLSession:didReceiveChallenge:completionHandler: delegate method of your NSURLSessionDelegate; which will give you the opportunity to match against the localhost url and manually trust it when the session data task begins (before it rejects due to the untrusted self-signed-certificate).

    Here's how:

    Step 1: Wrap the NSURLSession operations in a class, say "Networking", which implements the NSURLSessionDelegate protocol; and add the delegate method which manually trusts your localhost url (specified as 'DEV_URL').

    // Networking.h
    #import <Foundation/Foundation.h>
    
    @interface Networking : NSObject <NSURLSessionDelegate>
    - (void) fetchContentsOfUrl:(NSURL *)url
                     completion:(void (^)(NSData *data, NSError *error)) completionHandler;
    @end
    
    // Networking.m
    #import "Networking.h"
    
    static NSString *const DEV_URL = @"https://localhost:5001";
    @implementation Networking
    - (void) fetchContentsOfUrl:(NSURL *)url
                     completion:(void (^)(NSData * _Nullable, NSError * _Nullable))completionHandler {
        NSURLSession *dataSession = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:nil];
    
        NSURLSessionDataTask *dataTask = [dataSession
                                          dataTaskWithURL:url
                                          completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
    
                                              if (completionHandler == nil) {
                                                  return;
                                              }
    
                                              if (error){
                                                  completionHandler(nil, error);
                                                  return;
                                              }
    
                                              completionHandler(data, nil);
                                          }];
    
        [dataTask resume];
    }
    
    - (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler {
    
        if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
            NSURL* baseURL = [NSURL URLWithString:DEV_URL];
            if ([challenge.protectionSpace.host isEqualToString:baseURL.host]) {
                NSLog(@"trusting connection to host %@", challenge.protectionSpace.host);
                completionHandler(NSURLSessionAuthChallengeUseCredential, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]);
            } else {
                NSLog(@"Not trusting connection to host %@", challenge.protectionSpace.host);
                completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
            }
        }
        [challenge.sender continueWithoutCredentialForAuthenticationChallenge:challenge];
    }
    @end
    

    Step 2: Replace your original code with an instantiation of the Networking class and a call to the fetchContentsOfUrl:completion: method which invokes the NSURLSessionDataTask, as follows:

        Networking *networker = [[Networking alloc] init];
        [networker fetchContentsOfUrl:url completion:^(NSData *data, NSError *error) {        
            if (error == nil) {
                NSString *output = [NSString alloc];
                NSLog(@"%@", [output initWithData:data  encoding:(NSASCIIStringEncoding)] ? : @"no data");
            }
        }];
    

    You're all set!!