Child pages
  • iOS Code Example - SOAP over HTTPS with Client Certificate Authentication
Skip to end of metadata
Go to start of metadata

Overview

The following descriptions and code examples provide an overview of how we're currently implementing SOAP over HTTPS with client certificate authentication for iOS applications.  The entire code flow is included to provide a context to the lower-level details that appear toward the bottom.  Specifically, the "didReceiveAuthenticationChallenge" section is where credentials are extracted from the certificate and vetted with the challenger.

Service is Configured

When the application is first started, an instance of the service implementation will be initialized so it is ready for use by the client.  This is made available to the client via a variable that is available as an application scope variable, i.e., a "class" variable.

Service initialization
// inialize service

// create an instance of the service
myInstance.service = [SVCWebEaseService service];
// set logging to YES or NO (YES in dev, NO in prod)
myInstance.service.logging = NO;
// set the service account user name.  This is needed
// to access the web service initially
myInstance.service.username = @"webeasews";
// set service account password
myInstance.service.password = @"svc_acct_pw";
// set the service URL, i.e., the URL to the web service
myInstance.service.serviceUrl = @"https://www.app.emory.edu/axis2/services/WebEaseService";

// NOTE:  this information is set on the distribution certificate
// which is bundled with the app.  Once the user has a client/device
// specific certificate, these values will change to something 
// specific for that certificate.

// set the certificate type (file extension)
myInstance.service.certType = @"pfx";
// set the certificate name (file name without extension)
myInstance.service.certName = @"devclient";
// set the certificate password that will be used 
// to extract identity and trust info later
myInstance.service.certPassword = @"cert_pw";

Client Makes the Call

The client application will use the service to make a call for some backend service interaction, e.g., create, retrieve, update or delete some data.

Client Makes the Call
// Initialize the query parameter(s)
SVCLogEntryQuerySpecification *queryObj = [[SVCLogEntryQuerySpecification alloc] init];
queryObj.patientId = self.patientProfile.patientId;
queryObj.comparison = [[NSMutableArray alloc] init];
    
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setDateFormat:@"yyyy-MM-dd"];
[formatter setTimeZone:[NSTimeZone localTimeZone]];
    
SVCComparison *comparison = [[SVCComparison alloc] init];
comparison.operator = @"between";
comparison.fields = @"LogEntryDate";
comparison.data = @"StartDate,EndDate";
[queryObj.comparison addObject:comparison];
    
queryObj.startDate = fromDate;
queryObj.endDate = toDate;
logEntryQueryFromDate = fromDate;
logEntryQueryToDate = toDate;

SVCLogEntry *actionable = [[SVCLogEntry alloc] init];

// Invoke the appropriate service method, in this, we're querying for LogEntry objects
// We pass the delegate (self) as well as a reference to the handler that will process
// whatever results are returned from the service (or any errors).
// Additionally, we pass our populated query object and the object that will be used 
// to deserialize the server results and populate one of our "pojos" with that data
[[EUVariableStore sharedInstance].service LogEntryQuery:self 
													action:@selector(LogEntryQueryHandler:) 
													querySpec:queryObj 
													deserializer:actionable];

Service Implementation Initializes a SoapRequest object

A method like this is generated off of the WSDL associated to the web service facade being used.  In this case, the client is querying for LogEntry objects so this is the LogEntryQuery Objective-C implementation that was generated for that service method.

Initialize and Invoke SoapRequest
- (SoapRequest *) LogEntryQuery: (id) target action: (SEL) action querySpec: (SVCLogEntryQuerySpecification *) querySpec deserializer: (SVCLogEntry *) actionable {

	// Initialize and populate parameters
    NSMutableArray * _params = [NSMutableArray array];
    [_params addObject: [[SoapParameter alloc] initWithValue:querySpec 
												forName: @"emory:LogEntryQuerySpecification"]];

	// Create the SOAP envelop specific for this request
    NSString* _envelope = [Soap createEnvelope: @"LogEntryQueryRequest" 
									forNamespaces: nsdict 
									withParameters: _params 
									withHeaders: self.headers];

	// Initialize a SoapRequest instance
    SoapRequest* _request = [SoapRequest create: target 
											action: action 
											service: self 
											soapAction: @"http://www.emory.edu/WebEaseService/LogEntryQuery" 
											postData: _envelope 
											deserializeTo: actionable];

	// Call send method on the SoapRequest instance
    [_request send];

    return _request;
}

SoapRequest Send

The SoapRequest.send method checks that the client (device) is connected to the network and can reach the host (web service).  Then, it creates a NSMutableURLRequest object that contains the service URL, header information and the actual body of the request being sent.  In this example, the body of the data being sent is a LogEntry.Query.  Once it's populated the request object, it opens a NSURLConnection using the request data it's built and setting the delegate to "self" which tells the connection object to look in the SoapRequest object for the delegate method implementations that are associated to making a network connection (didReceiveResponse, didReceiveData etc.).

SoapRequest Send Method
// Sends the request via HTTP(S).
- (void) send {
    
    // If we don't have a handler, create a default
    if(handler == nil) {
        handler = [[SoapHandler alloc] init];
    }
    
    // Make sure the network is available (using helper class SoapReachability)
    if([SoapReachability connectedToNetwork] == NO) {
        NSError* error = [NSError errorWithDomain:@"OpenEAI" 
									code:400 
									userInfo:[NSDictionary 
									dictionaryWithObject:@"The network is not available" 
									forKey:NSLocalizedDescriptionKey]];
        [self handleError: error];
    }
    
    // Make sure we can reach the host (using helper class SoapReachability)
    if([SoapReachability hostAvailable:url.host] == NO) {
        NSError* error = [NSError errorWithDomain:@"OpenEAI" 
									code:410 
									userInfo:[NSDictionary 
									dictionaryWithObject:@"The host is not available" 
									forKey:NSLocalizedDescriptionKey]];
        [self handleError: error];
    }
    
    // Output the URL if logging is enabled
    if(logging) {
        NSLog(@"Loading: %@", url.absoluteString);
    }
    
    // Create the request which will be passed to the connection
	// when it is created later
	NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL: url];

	// Add SOAPAction http header to the request
	if(soapAction != nil) {
        [request addValue: soapAction 
					forHTTPHeaderField: @"SOAPAction"];
    }

	// Add other http headers to the request
	// This includes the SOAP data (envelop etc.) in the postData variable 
	// which was obtained from the client service and the serialized version of the
	// pojo object that was passed in.  This becomes the HTTPBody for the post request
	// we're about to make
	if(postData != nil) {
        [request setHTTPMethod: @"POST"];
        [request addValue: @"text/xml; charset=utf-8" forHTTPHeaderField: @"Content-Type"];
        [request setHTTPBody: [postData dataUsingEncoding: NSUTF8StringEncoding]];
        if(self.logging) {
            NSLog(@"%@", postData);
        }
    }
    
    // Create the connection passing the previously configured request object and "self" as the delegate.  
	// When the connection is created, all the delegate methods will be fired in the 
	// appropriate order (see NSURLConnection Delegate Methods code examples below)
    conn = [[NSURLConnection alloc] initWithRequest: request delegate: self];

	if(conn) {
        receivedData = [NSMutableData data];
    } else {
        // We will want to call the onerror method selector here...
        if(self.handler != nil) {
            NSError* error = [NSError errorWithDomain:@"SoapRequest" 
								code:404 
								userInfo: [NSDictionary 
								dictionaryWithObjectsAndKeys: @"Could not create connection", 
								NSLocalizedDescriptionKey,
								nil]];
            [self handleError: error];
        }
    }
}

NSURLConnection Delegate Methods

The delegate methods are fired as part of the process of sending the SOAP request via the NSURLConnection and the previously configured NSMutableURLRequest object.  One of the delegate methods that may be fired (will be fired if we're attempting to access an HTTPS URL) is called "didReceiveAuthenticationChallenge".  It is in this delegate method where identity information is extracted from our client certificate and presented to the challenger to vet this connection.

didReceiveResponse
// Called when the HTTP socket gets a response.
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {

	// reset our receivedData instance variable (NSMutableData) 
	// The response data from the server will go into this variable eventually
    [self.receivedData setLength:0];
}
didReceiveData
// Called when the HTTP socket received data.
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)value {

	// as we receive response data from the server, we append it 
	// to our 'receivedData' instance variable (NSMutableData)
    [self.receivedData appendData:value];
}
didFailWithError
// Called when the HTTP request fails.
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {

	// when/if there are errors making the connection, we'll reset the connection
	// and pass control to our generalized error handler (not shown)
    conn = nil;
    [self handleError:error];
}
canAuthenticateAgainstProtectionSpace
- (BOOL)connection:(NSURLConnection *)conn canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace *)protectionSpace
{
	// we always return yes here
    return YES;
}
didReceiveAuthenticationChallenge
// Called when/if the HTTP request receives an authentication challenge.
// This is the "meat" of the client certificate authentication code
-(void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {

	if([challenge previousFailureCount] == 0) {
        NSString* authMethod = [[NSString alloc] 
			initWithString:[[challenge protectionSpace] authenticationMethod]];
        
        if ([authMethod isEqual:@"NSURLAuthenticationMethodClientCertificate"]) {

			// create a variable that contains the path to our certificate 
			// Initially, the distribution certificate is bundled with the app
			// but it will be replace with a device/user specific cert once 
			// the user has registered
			NSString *PKCS12Path = [[NSBundle mainBundle]
                                    	pathForResource:self.certName 
										ofType:self.certType];

			// load the certificate data into a variable
			NSData *PKCS12Data = [[NSData alloc] initWithContentsOfFile:PKCS12Path];
            CFDataRef inPKCS12Data = (__bridge CFDataRef)PKCS12Data;
            
			// Variable declaration for variables that are used when the identity
			// is extracted from the certificate
			OSStatus status = noErr;
            SecIdentityRef identityRef;
            SecTrustRef trustRef;

			// extract identity info from the cert
			status = extractIdentityAndTrust(inPKCS12Data, 
												&identityRef, 
												&trustRef, 
												(CFStringRef)CFBridgingRetain(certPassword));
            
			// make a copy of the cert
            SecCertificateRef myCert = NULL;
            OSStatus certStatus = SecIdentityCopyCertificate(identityRef, &myCert);

			if (certStatus == 0) {
                SecCertificateRef certArray[1] = { myCert };
                CFArrayRef myCerts = CFArrayCreate(NULL, (void *)certArray, 1, NULL);

				// create a credential from the identity extracted and the copy of the cert
				NSURLCredential *credential = [NSURLCredential credentialWithIdentity:identityRef
                                                               certificates:(__bridge NSArray *)myCerts
                                                               persistence:NSURLCredentialPersistencePermanent];
                
				// attempt to use the credential thereby vetting the credential with the challenger
                [[challenge sender] useCredential:credential
                       				forAuthenticationChallenge:challenge];
                
                CFRelease(myCerts);
            }
            
            if(self.logging == YES) {
                NSLog(@"(SoapRequest) %@ authentication complete.", authMethod);
            }
        }
        else if ([authMethod isEqual:@"NSURLAuthenticationMethodServerTrust"]) {
            
			NSString *thePath = [[NSBundle mainBundle] 
									pathForResource:self.certName 
									ofType:self.certType];
            
            NSData *PKCS12Data = [[NSData alloc] initWithContentsOfFile:thePath];
            CFDataRef inPKCS12Data = (__bridge CFDataRef)PKCS12Data;
            
            OSStatus status = noErr;
            SecIdentityRef identityRef;
            SecTrustRef trustRef;
            status = extractIdentityAndTrust(inPKCS12Data, 
												&identityRef, 
												&trustRef, 
												(CFStringRef)CFBridgingRetain(certPassword));
            
            if (status == 0) {
                NSURLCredential *credential = [NSURLCredential credentialForTrust:trustRef];
                
                [[challenge sender] useCredential:credential
                					forAuthenticationChallenge:challenge];
            }
            
            if(self.logging == YES) {
                NSLog(@"(SoapRequest) %@ authentication complete.", authMethod);
            }
        }
        else {
            // For basic-auth: NSURLAuthenticationMethodDefault
            
            // this will be the service account user id and password which will be used
            // for basic auth
            NSURLCredential *newCredential = [NSURLCredential credentialWithUser:self.username 
																password:self.password 
																persistence:NSURLCredentialPersistenceNone];
            
            [[challenge sender] useCredential:newCredential
                   forAuthenticationChallenge:challenge];
            
            if(self.logging == YES) {
                NSLog(@"(SoapRequest) %@ authentication complete.", authMethod);
            }
        }
    } else {
        [[challenge sender] cancelAuthenticationChallenge:challenge];
        NSError* error = [NSError errorWithDomain:@"SoapRequest" code:403 
									userInfo: [NSDictionary 
										dictionaryWithObjectsAndKeys: @"Could not authenticate this request", 
									NSLocalizedDescriptionKey,
									nil]];
        [self handleError:error];
    }
}
extractIdentityAndTrust
// Helper method that extracts information from the certificate
OSStatus extractIdentityAndTrust(CFDataRef inPKCS12Data,
                                 	SecIdentityRef *outIdentity, 
									SecTrustRef *outTrust, 
									CFStringRef password) {

    OSStatus securityError = errSecSuccess;
    const void *keys[] =   { kSecImportExportPassphrase };
    const void *values[] = { password };
    CFDictionaryRef optionsDictionary = CFDictionaryCreate(
                                        	NULL, 
											keys,
                                            values, 
											1,
                                            NULL, 
											NULL);
    
    CFArrayRef items = CFArrayCreate(NULL, 0, 0, NULL);
    securityError = SecPKCS12Import(inPKCS12Data,
                                    optionsDictionary,
                                    &items);
    
    if (securityError == 0) {
        CFDictionaryRef myIdentityAndTrust = CFArrayGetValueAtIndex (items, 0);
        const void *tempIdentity = NULL;
        tempIdentity = CFDictionaryGetValue (myIdentityAndTrust,
                                             kSecImportItemIdentity);
        *outIdentity = (SecIdentityRef)tempIdentity;
        const void *tempTrust = NULL;
        tempTrust = CFDictionaryGetValue (myIdentityAndTrust, kSecImportItemTrust);
        *outTrust = (SecTrustRef)tempTrust;
    }
    
    if (optionsDictionary)
        CFRelease(optionsDictionary);
    
    return securityError;
}
connectionDidFinishLoading
// Called when the connection has finished loading.
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {

	// this is pattern/approach specific logic that we'll leave out of here for now
	// in our case, this is where we use the serializers/deserializers (objective-c pojos)
	// to parse the server response data and populate those pojos with the data that was returned.
	// ultimately, this is the data that gets passed back to the calling client
	// in the form of our objective-c pojos
}

 

 

  • No labels