Skip to content

Developer Guide - HttpConversation

The flexible HTTP networking stack of the SAP Mobile Platform SDK for iOS. In this document you'll find all the info necessary to start working with the HttpConversation and its close relative the HttpConvAuthFlows library. The guide gradually unveils the details as it goes from the most straightforward use cases to more complex ones. These two components are often referred to in the abbreviated form: HttpC.

It is assumed that you are familiar with iOS application development and Objective-C programming in general. Furthermore, a solid understanding of the NSURLSession framework is also more than recommended. A useful introduction can be found at the official Apple documentation site which can be handy if the built-in iOS HTTP networking stack is not that familiar to you.

Essentials

Sending basic GET and POST requests

Let's begin with the simplest example: firing an HTTP GET request against http://www.example.com:

#import "HttpConversationManager.h"

void example() {
    // Create a new instance of a 'HttpConversationManager'. This is the start of everything.
    HttpConversationManager* manager = [HttpConversationManager new];

    // Create the request object. This describes the URL at least but can also be used to
    // specify the HTTP method, the request headers and parameters and even the payload.
    // Note that because of the 'http' URL the App Transport Security settings in the
    // 'Info.plist' must allow unencrypted HTTP traffic to 'example.com'.
    NSURL* url = [NSURL URLWithString:@"http://www.example.com"];
    NSMutableURLRequest* req = [NSMutableURLRequest requestWithURL:url];

    // Send the request using the block-based API.
    [manager executeRequest:req completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) {
        if (error) {
            // Do something with the error that occurred which must have been something low-level, like
            // a network connectivity problem, a timeout, some kind of other IO error, etc..
            ...
        } else {
            // The 'response' contains the HTTP response data, including status information, response
            // headers, etc.. The 'data' is the actual payload.
            ...
        }
    }];
}

To get started, the very first step is always acquiring an instance of HttpConversationManager first. In simple cases one can instantiate one for itself and then throw it away at the end of the request. However, the manager may be reused for multiple requests.

Creating the request and processing the results is all done using built-in classes of the iOS SDK: NSURL, NSMutableRequest and NSURLResponse are all part of the iOS networking stack.

The reason behind is that when the executeRequest:completionHandler: method gets called on the manager, under the hood the library uses a properly configured NSURLSession instance to actually do the request sending and response processing.

Therefore, it is best to think of HttpConversationManager as an intelligent wrapper around NSURLSession extending the latter with additional features.

Let's send now a POST request to an endpoint that expects HTML form input with data of some user:

#import "HttpConversationManager.h"

void example() {
    // Obtaining a manager is the starting point.
    HttpConversationManager* manager = [HttpConversationManager new];

    // Create the request object. This time, some additional properties are also set.
    NSURL* url = [NSURL URLWithString:@"http://www.dummyservice.com/userform"];
    NSMutableURLRequest* req = [NSMutableURLRequest requestWithURL:url];
    req.HTTPMethod = @"POST";
    [req setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];

    // Set the payload. Note how the fields are properly encoded as required by the content type.
    NSString* userName = ...;
    NSString* emailAddress = ...;
    NSString* phoneNumber = ...;

    NSString* body = [NSString stringWithFormat:@"user_name=%@&email_address=%@&phone_number=%@",
                               [userName stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet],
                               [emailAddress stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet],
                               [phoneNumber stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
    req.HTTPBody = [body dataUsingEncoding:NSASCIIStringEncoding];

    // Send the request using the block-based API.
    [manager executeRequest:req completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) {
        // Process the results.
        ...
    }];
}

This concludes the basic examples. This should be more than enough to cover the simplest use cases. Let's continue by looking at an alternative way of handling requests.

Using delegates

The block-based API introduced in the previous section is very useful. However, its simplicity comes at a cost: the insight into the actual request execution can be pretty limited as only the final results are received in the block. In most of the cases, this is enough.

The NSURLSession API however specifies a handful of delegates protocols, such as NSURLSessionDelegate, NSURLSessionTaskDelegate, ...

Implementing these can give a deeper insight into what is going on. For example, if a larger piece of data is to be downloaded then one can keep track of the progress by implementing the appropriate delegate methods and updating a progress bar on the UI as bytes are transferred.

This guide does not intend to go into the details of how NSURLSession delegates should be used. The Apple-provided reference documentations already cover everything.

The HttpConversationManager class has a handful of properties using which delegates may be set. Let's do the previous HTTP GET example but with delegates this time to demonstrate these:

#import "HttpConversationManager.h"

@interface MyTaskDelegate <NSURLSessionTaskDelegate> @end
@implementation MyTaskDelegate

-(void)URLSession:(NSURLSession*)session
       dataTask:(NSURLSessionDataTask*)task
       didReceiveData:(NSData*)data {
    // Called whenever a piece of data has been received. This is the same data in fragments
    // that the block-based API would receive in one single 'NSData' objects. Here, one can
    // implement a progress bar UI update, for example.
    ...
}

-(void)URLSession:(NSURLSession*)session
       task:(NSURLSessionTask*)task
       didCompleteWithError:(NSError*)error {
    // Handle the completion of the task.
    ...
}

@end

void example() {
    // Create a new instance of a 'HttpConversationManager'. This is the start of everything.
    HttpConversationManager* manager = [HttpConversationManager new];

    // Create the request object. This describes the URL at least but can also be used to
    // specify the HTTP method, the request headers and parameters and even the payload.
    NSURL* url = [NSURL URLWithString:@"http://www.example.com"];
    NSMutableURLRequest* req = [NSMutableURLRequest requestWithURL:url];

    // Set the delegate.
    manager.sessionTaskDelegate = [MyTaskDelegate new];

    // Send the request using the delegate-based API.
    [manager executeRequest:req];
}

As it can be seen, the delegate-based approach yields more code and also makes it a bit more cumbersome to process the HTTP response. The block-based approach managed to confine all code related to sending the request within the example() method where it took place. With a delegate, however, we need to define our own delegate implementation (MyTaskDelegate in the above example) and pass it to the manager.

Consequently, it is recommended to rely on the block-based API as much as possible. Nonetheless, there are some complex use cases which can be implemented only with delegates reliably.

The advantage of HttpC over plain NSURLSession is that the two approaches may be combined. Let's modify again the above example a little.

#import "HttpConversationManager.h"

@interface ProgressUpdaterTaskDelegate <NSURLSessionTaskDelegate> @end
@implementation ProgressUpdaterTaskDelegate

-(void)URLSession:(NSURLSession*)session
       dataTask:(NSURLSessionDataTask*)task
       didReceiveData:(NSData*)data {
    // Update the progress bar here.
    ...
}

@end

void example() {
    // Create a new instance of a 'HttpConversationManager'. This is the start of everything.
    HttpConversationManager* manager = [HttpConversationManager new];

    // Create the request object. This describes the URL at least but can also be used to
    // specify the HTTP method, the request headers and parameters and even the payload.
    NSURL* url = [NSURL URLWithString:@"http://www.example.com"];
    NSMutableURLRequest* req = [NSMutableURLRequest requestWithURL:url];

    // Set the delegate.
    manager.sessionTaskDelegate = [ProgressUpdaterTaskDelegate new];

    // Send the request using the block-based API.
    [manager executeRequest:req completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) {
        if (error) {
            // Do something with the error that occured which must have been something low-level, like
            // a network connectivity problem, a timeout, some kind of other IO error, etc..
            ...
        } else {
            // The 'response' contains the HTTP response data, including status information, response
            // headers, etc.. The 'data' is the actual payload.
            ...
        }
    }];
}

This time, the block-based API is used to consume the final response whereas the delegate is used to "look into" the request execution. Our ProgressUpdaterTaskDelegate class no longer bothers with processing the response. It simply takes a look into the next chunk being downloaded and updates the progress UI.

Combining the use of blocks and delegates is a recommended approach for use cases like the above one.

Summary

The following things are to be taken away from this introductory section:

  • HttpConversation builds on top of the built-in NSURLSession by offering a quite similar API enhanced with some additional features.
  • The starting point is always a HttpConversationManager instance. For now, look at it as a factory using which requests can be created.
  • To actually send and receive HTTP requests and responses, either a block- or a delegate-based API may be used. However, it is possible to combine the two approaches into a single elegant solution.
  • The classes using which data can be read/written, requests can be modified and responses can be processed are also built-in ones. No need to learn new things in this regard.

Philosophy and features

Let's continue by taking a look at the main philosophy of the library and its features. This section is followed by a series of examples.

The HttpConversationManager class

In previous examples, the conversation manager has always been instantiated anew and has not been reused. The examples could have been modified to make use of a single HttpConversationManager object stored in a property of a class or a global variable, for example. As far as the introductory code snippets are concerned, the results would have been the same.

What does a single instance of a HttpConversationManager represent then? Apart from being just a factory to create and send HTTP requests with, it also carries a series of user exits that can be utilized to customize the behaviour of and/or hook into the various stages of HTTP request/response processing.

Each of these user exits have corresponding properties and methods on this class which are categorized as follows:

  • NSURLSession delegates can be configured via the corresponding properties and are useful if one wishes to hook into the request/response processing at the lowest abstraction level.
  • Filters are standardized hooks invoked at predefined points. The 3 subtypes: request, response and challenge filters may be used for different purposes. There are methods available to add and remove filters.
  • Observers can be added the same way as filters can but they can only be used for getting notifications about certain events. It is not possible to alter the way how the HTTP request is being sent via observers.

A particular collection of user exits (delegates, filters, observers) together are called the configuration of a particular HttpConversationManager instance.

The most important implication of this is that when thinking about how to manage manager instances within an application, one actually has to think about the configurations.

What can these user exits be used for? Many things, but in the overwhelming majority of the cases a configuration is used to prepare for handling authentication challenges transparently.

"Transparently" here refers to that with a properly configured HttpConversationManager one can separate the logic required to handle authentication and authorization with the server from the logic that sends HTTP requests to acquire actual data or perform changes on a server.

This might be shady at this point but reading on will help clear things up.

The concept of a conversation

The HttpC library introduces a new concept, called the HTTP conversation (hence the name). A conversation might consist of multiple HTTP request-response exchanges with the server 'till the process settles down and produces a final response.

How many requests are sent depends heavily on how a HttpConversationManager is configured. But first, let's take a look at the lifecycle of a conversation:

  1. A conversation starts by invoking one of the execute* methods of the manager.
  2. Before the actual request is executed, the manager calls all the configured request filters in the order they were added. These filters get a chance to take a look into the request that is about to be sent. Some filters might even alter the request by adding request headers or additional parameters.
  3. The HTTP request is then sent using the underlying NSURLSession object. During this phase many things can happen:
    • The configured delegates are called according to the contracts of the NSURLSession API.
    • If an authentication challenge is detected that the NSURLSession API supports (mostly: Basic Authentication, X.509 client certificate authentication) then the challenge is handed over to the sequence of challenge filters configured. These are processed in sequence just like request filters but with a different purpose: the first one to successfully produce some credentials wins and the original request is restarted, this time sending the credentials too. The conversation then jumps back to step no. 2.
  4. After the response has been received, each configured response filter is called in order, one after the other. These can perform HTTP response post-processing of any kind. If one of these filters commands the executing conversation manager to restart the request then, just like in case of authentication challenges, the conversation jumps back to step no. 2.
  5. At the very end of the conversation, the final response (i.e. the response of the last HTTP request fired) is handed over to whoever initiated the request. If the block-based API has been used then it gets called only at this point.

Distinguishing between a single HTTP request and the entire HTTP conversation is therefore crucial in understanding how the library works.

Let's take a look again at the HTTP GET introductory example but this time let's extend it with some configuration:

#import "HttpConversationManager.h"

static HttpConversationManager* gManager;

@interface HardCodedCredentialFilter <ChallengeFilterProtocol> @end
@implementation HardCodedCredentialFilter

-(void)handleChallenge:(NSURLAuthenticationChallenge*)challenge
       conversationManager:(HttpConversationManager*)conversationManager
       completionBlock:(challenge_filter_completion_t)completionBlock {
    if (challenge.previousFailureCount == 0)
        // The credentials have not been tried yet. Let's return it.
        completionBlock(YES, [NSURLCredential credentialWithUser:@"john_doe"
                                              password:@"p4Ssw0rD1Z3"
                                              persistence:NSURLCredentialPersistenceNone]);
    else
        // The hard-coded credentials seem to have failed. We no longer know how to authenticate
        // properly with this server.
        completionBlock(NO, nil);
}

@end

void configure() {
    // Create the manager instance.
    gManager = [HttpConversationManager new];

    // Add the challenge filter to it.
    [gManager addChallengeFilter:[HardCodedCredentialFilter new]];
}

void example() {
    // Create the request object. This describes the URL at least but can also be used to
    // specify the HTTP method, the request headers and parameters and even the payload.
    NSURL* url = [NSURL URLWithString:@"http://www.example.com"];
    NSMutableURLRequest* req = [NSMutableURLRequest requestWithURL:url];

    // Send the request using the block-based API. Note that we're making use of 'gManager', therefore
    // 'configure()' has to be called once before this method.
    [gManager executeRequest:req completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) {
        if (error) {
            // Do something with the error that occurred which must have been something low-level, like
            // a network connectivity problem, a timeout, some kind of other IO error, etc..
            ...
        } else {
            // The 'response' contains the HTTP response data, including status information, response
            // headers, etc.. The 'data' is the actual payload.
            ...
        }
    }];
}

Note how the act of configuring the manager is detached from actually using the manager: the configure() method should be called only once after which the gManager static variable will point to the properly configured conversation manager. After that, example() can be called as many times we want. It will make use of the configured manager.

Of course, the above example is not so nice as it uses plain C functions and global variables. This is just to cut down on the syntactical noise for the purpose of this example.

Note how in the example() method we don't bother with authentication at all. The request is just assembled and the results are processed in the completion block. There's no need for this method to know the credentials or let alone be aware of how the authentication needs to be performed. This is all taken care of by the configure() method which applies our HardCodedCredentialFilter on the newly instantiated manager.

Also note that the HardCodedCredentialFilter prepares for that it might be called multiple times. Checking the previousFailureCount of the challenge object ensures that the hard-coded credentials are tried only once, thereby preventing an infinite loop of retries.

Summary

Below is what you should remember from this part:

  • An HttpConversationManager instance carries a configuration that affects how HTTP request and responses are handled.
  • The configuration is made up of delegates, filters and observers.
  • A conversation is actually a complex flow which invokes the configured user exits and produces a final response. During execution multiple HTTP requests might get sent, for example, in case authentication challenges need to be responded to.
  • The main philosophy therefore is that configuring and using a conversation manager can be separated.

A closer look

In this section we begin to take a step further and examine the features of HttpC more closely. We'll discuss the various task types and the possible ways of configuring a HttpConversationManager.

Data, download and upload tasks

The NSURLSession API defines the following tasks types that HttpC also makes use of:

  • Data tasks, for simple HTTP GET/POST/PUT/DELETE/... operations which usually involve a smaller payload in the request and the response. One would use this to access HTTP REST endpoints that accept and/or produce OData, JSON, XML, etc.. content. The key characteristic of data tasks is that these payloads are relatively small.
  • Download tasks, for downloading large (i.e. above several hundred megabytes) payloads which can be accessed either as a file or as a stream. These tasks can be cancelled and then later resumed. Often, these kinds of tasks are executed in the background.
  • Upload tasks, for uploading large payloads to an HTTP endpoint. Like download tasks, uploads are also often organized into background tasks. The source of the upload is either some predefined data (i.e. NSData) or a stream.

The conversation manager class offers various execute* methods to start tasks with. However, not all task types support both blocks and delegates. The below table summarizes what is supported.

Note that this guide sometimes says starting tasks, sometimes starting conversations. The two things refer to the same thing, even though under the hood a single conversation might consist of multiple HTTP requests, consequently, of multiple NSURLSessionTasks.

Task type Block-based method Delegate-based method
Data task executeRequest:completionHandler: executeRequest:
Download task (to file) N/A executeDownloadRequest:
executeDownloadRequestWithResumeData:
Download task (to stream) executeStreamedDownloadRequest:completionHandler: N/A
Upload task (from data) N/A executeUploadRequest:
Upload task (from stream) executeStreamedUploadRequest:completionHandler: N/A

A delegate-based download means that one has to set the proper download task delegate (i.e. NSURLSessionDownloadDelegate) on the HttpConversationManager instance. When the download completes, the URLSession:downloadTask:didFinishDownloadingToURL: delegate method is called with the URL pointing to the downloaded file.

Similarly, a delegate-based upload also requires a delegate to be set on the conversation manager. In this case an NSURLSessionTaskDelegate is to be set to the sessionTaskDelegate property. Note that the NSURLSession API defines no such thing as NSURLSessionUploadDelegate...

Uploading an NSData can be done by setting the HTTPBody property of the NSMutableURLRequest object we create for the upload and then call executeUploadRequest: on the conversation manager. During the started conversation, if the upload needs to be attempted multiple times because of intermittent authentication challenges then the same NSData payload will be sent every time. The advantage of this approach is its simplicity but this comes at the cost that the entire payload must be loaded into memory.

Uploading from a stream is more memory efficient. When creating the NSMutableURLRequest one can set its HTTPBodyStream property to an input stream from which HttpC will take the payload when doing the upload. However, this solution implies that if the conversation is restarted then the stream cannot be reused. Take a look at the below code:

#import "HttpConversationManager.h"

void example() {
    // Obtain a conversation manager from somewhere.
    HttpConversationManager* manager = ...;

    // Create an input stream to read data from. Typically, one would create a stream
    // for reading the contents of a file.
    NSInputStream* payloadStream = ...;

    // Create the request object and set a body stream for it.
    NSURL* url = [NSURL URLWithString:@"http://www.example.com/upload"];
    NSMutableURLRequest* req = [NSMutableURLRequest requestWithURL:url];
    req.HTTPBodyStream = payloadStream;

    // Send the request using the streamed upload API.
    [manager executeStreamedUploadRequest:req completionHandler:^(NSURLResponse* response, NSError* error) {
        ...
    }];
}

Now, everything is okay if the conversation started this way fires a single HTTP request. However, if an authentication challenge occurs then there's no way to reuse the stream as (generally) it cannot be rewound to the very first byte. A stream once exhausted may never be read from again. The above code therefore should be used only if the conversation is guaranteed not to be restarted by HttpC.

The bullet-proof solution would be the following:

#import "HttpConversationManager.h"

@interface UploadingDelegate <NSURLSessionTaskDelegate> @end
@implementation UploadingDelegate

-(void)URLSession:(NSURLSession*)session
       task:(NSURLSessionTask*)task
       needNewBodyStream:(void (^)(NSInputStream*))completionHandler {
    // Create the input stream at this point.
    NSInputStream* payloadStream = ...;

    // Give it to the caller.
    completionHandler(payloadStream);
}

@end

void example() {
    // Obtain a conversation manager from somewhere.
    HttpConversationManager* manager = ...;

    // Set the delegate.
    manager.sessionTaskDelegate = [UploadingDelegate new];

    // Create the request object. At this point, the body stream is not set.
    NSURL* url = [NSURL URLWithString:@"http://www.example.com/upload"];
    NSMutableURLRequest* req = [NSMutableURLRequest requestWithURL:url];

    // Send the request using the streamed upload API.
    [manager executeStreamedUploadRequest:req completionHandler:^(NSURLResponse* response, NSError* error) {
        ...
    }];
}

The end result is that the streamed upload will turn to our delegate at the point when the upload of the payload begins. The UploadingDelegate needs to be prepared that its URLSession:task:needNewBodyStream: method might be invoked multiple times (if there are multiple HTTP requests in the conversation due to restarts).

Note that when creating the NSMutableURLRequest we did not set the HTTPBodyStream property. This is not a strict requirement, we could have set it. In this case HttpC would have tried to upload the stream set in this property first and turn to the delegate only if the conversation is restarted and a new stream is needed.

Don't be confused that streamed upload is regarded as block-based whereas it might require a delegate to be specified as well. This is because only the end of the upload is signaled via the block, acquiring the stream is what the delegate is needed for. Remember that blocks and delegates may be combined, as documented here. This is just another example for this kind of combination.

App Transport Security considerations

Just as a reminder, in recent iOS version Apple introduced App Transport Security, a mechanism that can restrict what endpoints an application has permission to access. HttpC is not exempt from these restrictions, therefore ATS settings in the Info.plist file of the app must be set correctly before trying to send requests with a HttpConversationManager. This is especially true if the app tries to access servers via unencrypted HTTP. The default ATS settings will not allow this, causing the application to crash with an exception.

Make sure to review your ATS settings and configure them correctly for the endpoints you wish to access from the app before sending any requests using HttpC.

Session configuration

An instance of an NSURLSession can be created using an NSURLSessionConfiguration object. This can be used to configure a lot of things, like what cookie store to be used, what the timeout should be, how to cache certain credentials, etc.. Take a look at the Apple API reference for the details.

The HttpConversationManager class defines the initWithSessionConfiguration: initializer through which a custom session configuration may be specified. This will be used to create the underlying NSURLSession object.

From this aspect, there's not much else to say. If you're familiar with how an NSURLSession object is configured then you already know how to configure a HttpConversationManager.

The only thing of note are background session configurations. Starting background session tasks requires quite some coding from the application developer: the background session configuration needs to be given an identifier, certain special callback methods must be implemented properly in the UIApplicationDelegate, the session object needs to be reinstantiated with the same background identifier, etc.. Look at the official Apple documentations to understand how to do this properly.

Using background configurations is also possible with HttpC. The key difference compared to using vanilla NSURLSession is that you must instantiate a HttpConversationManager instead with the proper configuration and delegates configured.

Manager configurators

The word configuration might feel a bit overloaded. If you have read the previous sections then you should have come across two kinds of configurations with respect to a HttpConversationManager:

  1. The session configuration, parametrizing the underlying NSURLSession.
  2. The manager configuration, represented by the various filters, observers and delegates added to the manager.

In the previous section, we talked about the first one. This section is about the second.

The HttpConversation library defines a special protocol, called ManagerConfiguratorProtocol which defines a single method: configureManager:. This protocol is adopted by classes which have the responsibility of configuring HttpConversationManager instances by adding filters, delegates and/or observers to it, as needed.

Classes implementing ManagerConfiguratorProtocol are therefore called manager configurators. Using these classes is not strictly required but they may come in handy. The most notable (and probably the most frequently used) implementation is the CommonAuthenticationConfigurator class found in the HttpConvAuthFlows library. However, applications themselves can define their own manager configurators if they frequently instantiate conversation managers which need to be configured the same way.

Let's look at an earlier example and rewrite it using configurators:

#import "HttpConversationManager.h"
#import "ManagerConfiguratorProtocol.h"

@interface HardCodedCredentialFilter <ChallengeFilterProtocol> @end
@implementation HardCodedCredentialFilter

-(void)handleChallenge:(NSURLAuthenticationChallenge*)challenge
       conversationManager:(HttpConversationManager*)conversationManager
       completionBlock:(challenge_filter_completion_t)completionBlock {
    if (challenge.previousFailureCount == 0)
        // The credentials have not been tried yet. Let's return it.
        completionBlock(YES, [NSURLCredential credentialWithUser:@"john_doe"
                                              password:@"p4Ssw0rD1Z3"
                                              persistence:NSURLCredentialPersistenceNone]);
    else
        // The hard-coded credentials seem to have failed. We no longer know how to authenticate
        // properly with this server.
        completionBlock(NO, nil);
}

@end

static id<ManagerConfiguratorProtocol> _configurator;

@interface FooAppConfigurator <ManagerConfiguratorProtocol> @end
@implementation FooAppConfigurator

+(id<ManagerConfiguratorProtocol>)sharedInstance {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _configurator = [FooAppConfigurator new];
    });
    return _configurator;
}

-(void)configureManager:(HttpConversationManager*)manager {
    [manager addChallengeFilter:[HardCodedCredentialFilter new]];
}

@end

void example() {
    // Create the manager.
    HttpConversationManager* manager = [HttpConversationManager new];

    // Configure it using our app-specific configurator.
    [[FooAppConfigurator sharedInstance] configureManager:manager];

    // Create the request object. This describes the URL at least but can also be used to
    // specify the HTTP method, the request headers and parameters and even the payload.
    NSURL* url = [NSURL URLWithString:@"http://www.example.com"];
    NSMutableURLRequest* req = [NSMutableURLRequest requestWithURL:url];

    // Send the request using the block-based API. Note that we're making use of 'gManager', therefore
    // 'configure()' has to be called once before this method.
    [gManager executeRequest:req completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) {
        if (error) {
            // Do something with the error that occurred which must have been something low-level, like
            // a network connectivity problem, a timeout, some kind of other IO error, etc..
            ...
        } else {
            // The 'response' contains the HTTP response data, including status information, response
            // headers, etc.. The 'data' is the actual payload.
            ...
        }
    }];
}

With the use of a manager configurator we can clearly encapsulate the logic of configuring a HttpConversationManager into a single class. In the example() function, all we have to do is create a manager and then configure it using FooAppConfigurator (in our example we made it a singleton as it is stateless).

One might ask why couldn't we just simply use a single conversation manager, configure it once and then be done with the whole thing? The reason is that oftentimes the same conversation manager cannot be used for all the requests in the app. This is especially the case if delegates have to be used: you cannot specify a delegate per conversation, you have to set it as a property of the conversation manager.

However, even if a single conversation manager is enough for an app, using manager configurators is still advisable because of the clarity. It is up to you how you'd like to organize the related code. It's possible to simply use CommonAuthenticationConfigurator, write your own configurator like in the above example or do both: create an app-specific configurator which under the hood makes use of the HttpConvAuthFlows library to actually configure the conversation manager. It all depends on what level of flexibility you need in your app.

However, overcomplicating your code with this optional component is not recommended. The vast majority of apps configure their conversation managers somewhere around application startup by using CommonAuthenticationConfigurator to enable certain authentication types. That should be more than enough for most of the apps.

Summary

  • The HttpC API can be used to start data, download and upload tasks via the various execute* methods. The implications and the use of blocks and/or delegates have to be carefully understood beforehand, especially in case of upload tasks.
  • Just like with NSURLSession, the App Transport Security settings have to be set properly, otherwise the system will not permit the communication that the application tries to initiate.
  • Configuring a HttpConversationManager can be done from two aspects:
    • Configuring the underlying NSURLSession via NSURLSessionConfigurations. There's not much difference here compared to how a vanilla URL session would be configured. A bit of extra attention is needed though when using background session configurations. Read the above guide for the details.
    • Configuring the manager itself directly (by manually adding filters, observers, etc. to the manager) or via manager configurators. The latter is for convenience but HttpC ships the CommonAuthenticationConfigurator class that knows how to configure the most frequently used authentication and authorization types.

Common authentication/authorization flows

Commonly used authentication and authorization flows are organized into the HttpConvAuthFlows library. It is built on top of HttpConversation and its main class is CommonAuthenticationConfigurator which is a manager configurator implementation using which application developers can configure these flows with ease.

The configurator itself can be parametrized with various options and user exits. The next subsections describe the details about each supported flow.

It's recommended to take a look at the API documentation of CommonAuthenticationConfigurator which also contains a lot of useful info. This class will simply be referred to as the configurator or common configurator hereafter.

Basic Authentication

IMPORTANT NOTE: Before considering the use of the Basic Authentication scheme, be advised that it has its inherent limitations (arising either from its simplicity or from ambiguities caused by the standard). These are:

This scheme can work reliably only with usernames and passwords which do not contain characters outside the US-ASCII range. This is because certain platforms encode the BasicAuth header with a different encoding. Unfortunately, the standard is not very clear from this aspect and thus certain client and server solutions use different encodings. For example, iOS native applications encode the header using ISO-8859-1 encoding whereas Android native applications use UTF-8.

Therefore, use the BasicAuth scheme only if you understand these limitations and when you don't plan to let your end users pick credentials that might contain Unicode characters outside the US-ASCII range.

If you insist on using non-ASCII characters in credentials then we urge you to switch to another authentication method, like SAML2. Otherwise, you will not be able to build a bullet-proof solution on top of BasicAuth. You have been warned! * Usernames are separated from passwords in the BasicAuth HTTP request header using the colon : character. It is highly recommended to disallow the use of this character in user credentials too to avoid parsing errors of the credentials on the server side. * As credentials are transferred effectively in plain-text, it is not recommended to use this scheme over unencrypted HTTP.

The BasicAuth scheme is supported by NSURLSession out of the box. However, to make it work one would have to implement the appropriate session task delegate methods.

Instead of doing that, HttpConvAuthFlows can be used to implement an easier solution via the addUsernamePasswordProvider: method of the configurator. Use this method to add a so-called username/password provider (i.e. an implementation of the UsernamePasswordProviderProtocol). When a challenge occurs, the provider is going to be called which then can either produce the correct credentials or return nothing (which of course cancels the conversation).

As multiple providers may be added, the first one that succeeds in returning credentials wins and the iteration over providers is stopped.

Let's modify our hard-coded credential example with something more realistic:

#import "HttpConversationManager.h"
#import "CommonAuthenticationConfigurator.h"
#import <UIKit/UIKit.h>

static HttpConversationManager* gManager;

@interface UserPassDialogProvider <UsernamePasswordProviderProtocol> @end
@implementation UserPassDialogProvider

-(void)provideUsernamePasswordForAuthChallenge:(NSURLAuthenticationChallenge*)authChallenge 
       completionBlock:(username_password_provider_completion_t)completionBlock {
    // The BasicAuth challenge has been received. Show the dialog.
    UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"Enter credentials" 
                                                  message:@"Please enter your credentials"
                                                  preferredStyle:UIAlertControllerStyleAlert];

    // Add the input fields for username and password input.
    [alert addTextFieldWithConfigurationHandler:^(UITextField* textField) {
        textField.placeholder = @"User name";
    }];
    [alert addTextFieldWithConfigurationHandler:^(UITextField* textField) {
        textField.secureTextEntry = YES;
        textField.placeholder = @"Password";
    }];

    // Add the 'Login' action.
    [alert addAction:[UIAlertAction actionWithTitle:@"Login"
           style:UIAlertActionStyleDefault
           handler:^(UIAlertAction* action) {
        // Do nothing if some inputs are missing.
        UITextField* usernameField = alert.textFields[0];
        UITextField* passwordField = alert.textFields[1];
        if (!usernameField.hasText || !passwordField.hasText) {
            return;
        }

        // Success. Return the credentials.
        completionBlock([NSURLCredential credentialWithUser:usernameField.text 
                                         password:passwordField.text 
                                         persistence:NSURLCredentialPersistenceForSession], nil);
    }]];

    // Add the 'Cancel' action.
    [alert addAction:[UIAlertAction actionWithTitle:@"Cancel"
           style:UIAlertActionStyleCancel
           handler:^(UIAlertAction* action) {
        // No credentials have been provided.
        completionBlock(nil, YES);
    }]];

    // Present the alert view controller. Ensure that it is done on the UI thread.
    dispatch_async(dispatch_get_main_queue(), ^{
        UIViewController* parent = ...; // Get the parent controller from somewhere.
        [parent presentViewController:alert animated:YES completion:nil];
    });
}

@end

void configure() {
    // Create the manager instance.
    gManager = [HttpConversationManager new];

    // Create and parametrize the configurator.
    CommonAuthenticationConfigurator* configurator = [CommonAuthenticationConfigurator new];
    [configurator addUsernamePasswordProvider:[UserPassDialogProvider new]];

    // Configure the manager.
    [configurator configureManager:gManager];
}

void example() {
    // Create the request object. This describes the URL at least but can also be used to
    // specify the HTTP method, the request headers and parameters and even the payload.
    NSURL* url = [NSURL URLWithString:@"http://www.example.com"];
    NSMutableURLRequest* req = [NSMutableURLRequest requestWithURL:url];

    // Send the request using the block-based API. Note that we're making use of 'gManager', therefore
    // 'configure()' has to be called once before this method.
    [gManager executeRequest:req completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) {
        if (error) {
            // Do something with the error that occurred which must have been something low-level, like
            // a network connectivity problem, a timeout, some kind of other IO error, etc..
            ...
        } else {
            // The 'response' contains the HTTP response data, including status information, response
            // headers, etc.. The 'data' is the actual payload.
            ...
        }
    }];
}

The above example leverages that UsernamePasswordProviderProtocol is a block-based asynchronous protocol. It allows us to create and present an alert view that asks for the credentials. Meanwhile the dialog is up and the user is interacting with it, the conversation is waiting. At this point no network resources are held as we're actually in between two HTTP requests.

Note how the example UserPassDialogProvider takes care of submitting any UI related calls to the main thread. This is needed as HttpC executes these user exits from background threads.

A key challenge in writing an appropriate, UI-relevant user exit like the one in the above example is making sure the view controller on top of which we'd like to show our UI is accessible somehow. General design patterns do not exist for this problem. A possible solution would be to configure a conversation manager for every screen. This way, the configured user exits can access the parent view controller rather easily.

Make sure to carefully design how configurators, HttpConvAuthFlows user exits and UIViewController references are managed. This is one of the most important things to get right in an application that uses HttpC.

This problem also applies to other auth. types supported by HttpConvAuthFlows that define asynchronous credential providers.

Of course, we can easily integrate our previous hard-coded credentials into the flow. The solution would be to create another class that implements UsernamePasswordProviderProtocol which returns the credentials unconditionally:

...
@interface HardCodedCredentialProvider <UsernamePasswordProviderProtocol> @end
@implementation HardCodedCredentialProvider

-(void)provideUsernamePasswordForAuthChallenge:(NSURLAuthenticationChallenge*)authChallenge 
       completionBlock:(username_password_provider_completion_t)completionBlock {
    if (challenge.previousFailureCount == 0)
        // The credentials have not been tried yet. Let's return it.
        completionBlock([NSURLCredential credentialWithUser:@"john_doe"
                                         password:@"p4Ssw0rD1Z3"
                                         persistence:NSURLCredentialPersistenceNone], nil);
    else
        // The hard-coded credentials seem to have failed. We no longer know how to authenticate
        // properly with this server.
        completionBlock(nil, nil);
}

@end

void configure() {
    ...

    // Create and parametrize the configurator.
    CommonAuthenticationConfigurator* configurator = [CommonAuthenticationConfigurator new];
    [configurator addUsernamePasswordProvider:[HardCodedCredentialProvider new]];
    [configurator addUsernamePasswordProvider:[UserPassDialogProvider new]];

    ...
}

...

Note that the provider implementation still has to account for the failure count, just like our HardCodedCredentialFilter implementation had to in one of our previous examples. The user exits of HttpConvAuthFlows are not exempt from having to do proper state management. Never forget that these user exits run within the scope of a conversation.

Also note that the order in which the providers are added to the configurator does in fact matter. The above example will try to use the hard-coded credentials first and only fall back to showing the dialog if those fail.

Mutual SSL/TLS authentication

In other words: X.509 client certificate authentication. This is when during the SSL/TLS handshake the server asks the client to present a suitable client certificate.

This authentication type also has its own protocol: ClientCertProviderProtocol which can be configured using the addClientCertProvider: method of the common configurator.

Just like BasicAuth, multiple providers may be added in sequence. Each has a similar block-based asynchronous signature like the username/password provider, allowing the implementor to write any kind of asynchronous code that can produce a certificate.

Although possible, it is not recommended to undertake lengthy actions from within a client certificate provider. This is because unlike BasicAuth, the conversation is waiting for the provider in the middle of the SSL/TLS handshake. If a timeout occurs then the request fails, even though the certificate provider might eventually produce a valid certificate.

As this authentication type is also supported by NSURLSession out of the box, the certificates have to be produced in the form of NSURLCredential objects again which, in turn, require that the certificate be represented using a SecIdentityRef. This implies that providing certificates requires one to be familiar with the iOS KeyChain API.

Let's alter the BasicAuth example to use a certificate provider instead. The certificate is taken from a password-protected P12 file. Only the relevant code is demonstrated. We assume that by this time you are already familiar with how the rest works.

...

@interface FileBasedCertificateProvider <ClientCertProviderProtocol> @end
@implementation FileBasedCertificateProvider

-(void)provideClientCertForAuthChallenge:(NSURLAuthenticationChallenge*)authChallenge
       completionBlock:(client_cert_provider_completion_t)completionBlock {
    // Read the contents of the file.
    NSData* pkcs12Data = [NSData dataWithContentsOfFile:@"<path/to/P12/file/cert.p12>"];

    // Pass the parameters on to the Core Foundation layer.
    CFDataRef cPkcs12Data = (__bridge_retained CFDataRef)pkcs12Data;
    CFDictionaryRef options = NULL;

    // Create the input dictionary.
    CFStringRef cPassword = (__bridge CFStringRef) @"FileP4ssW0rD";
    const void* keys[] = { kSecImportExportPassphrase };
    const void* values[] = { cPassword };
    options = CFDictionaryCreate(NULL, keys, values, 1, NULL, NULL);

    // Perform the PKCS12 input.
    CFArrayRef items = CFArrayCreate(NULL, 0, 0, NULL);
    OSStatus securityError = SecPKCS12Import(cPkcs12Data, options, &items);

    // Release the resources no longer needed.
    CFRelease(cPkcs12Data);
    CFRelease(options);

    // Evaluate the results.
    if (securityError == errSecSuccess) {
        // Extract the reference to the identity variable.
        CFDictionaryRef identityDict = CFArrayGetValueAtIndex(items, 0);
        SecIdentityRef identity = (SecIdentityRef)CFDictionaryGetValue(identityDict, kSecImportItemIdentity);

        // Get the corresponding certificate.
        SecCertificateRef certificate = nil;
        SecIdentityCopyCertificate(identity, &certificate);

        // Put the certificate in an array.
        SecCertificateRef certs[1] = { certificate };
        CFArrayRef certificateArray = CFArrayCreate(NULL, (const void **)certs, 1, NULL);

        // Create the credential object with what we have so far.
        NSURLCredential* credentials = [NSURLCredential credentialWithIdentity:identity
                                                        certificates:(__bridge_transfer NSArray*)(certificateArray)
                                                        persistence:NSURLCredentialPersistenceForSession];

        // Release some resources.
        CFRelease(items);

        // Send back the acquired credentials.
        completionBlock(credentials, nil);
    } else {
        // Clean up.
        if (items) {
            CFRelease(items);
        }

        // Report failure.
        completionBlock(nil, nil);
    }
}

@end

void configure() {
    ...

    // Create and parametrize the configurator.
    CommonAuthenticationConfigurator* configurator = [CommonAuthenticationConfigurator new];
    [configurator addClientCertProvider:[FileBasedCertificateProvider new]];

    ...
}

...

Quite a mouthful... The good thing is that as it's using built-in Apple APIs, every solution that can produce a certificate in the form of a SecIdentityRef or NSURLCredential can be easily integrated with this protocol. Given the length of the above snippet there's chance that such solutions are already available, either in your app already or as open-source components, for example.

If your app already does certificate-based authentication somehow then migrating it to HttpC shouldn't be too much of a trouble.

SAML2

SAML2 is a widespread and rather comprehensive standard for various purposes, including single sign-on, identity federation, etc.. An overview of the standard can be found at the official Oasis documentation site which is a must-read before reading this section as the nomenclature used below makes heavy use of the concepts laid down by the standard.

Overview

The SAML2 support in HttpC allows a mobile application developer to integrate the application with a SAML2-based web single sign-on infrastructure. This is the use case what the standard commonly refers to as the Web Browser SSO Profile. This actually boils down to displaying a web-based login form of some sort using which the user can authenticate and then access the protected resource.

HttpC is a native networking solution. If a SAML2-protected resource needs to be accessed on the Service Provider (SP) then an authenticated session must be established beforehand. The goal of SAML2 support is therefore to detect the need for the authentication, orchestrate the web-based login and then resume the original request that targeted the protected resource.

Originally, the SAML2 web SSO profile has been designed purely for browser-based applications. There, accessing a protected resource is most often done by opening a link. If an authentication is required then the SP redirects the browser to an Identity Provider (IdP) which handles the authentication and then redirects back to the SP. At the end, the browser is redirected to the original resource but this time an authenticated session is already present.

With a little modification, the above flow is reusable for native requests (i.e. those which are initiated by a HTTP client that is not a web browser). Here's how all this takes place using HttpC using a properly configured HttpConversationManager:

  1. One of the execute* methods is used to send a request (i.e. start a new conversation). The type (data, download, upload) or whether blocks or delegates are used, does not matter. The request targets a protected endpoint hosted by the SP.
  2. The SP responds with an authentication challenge, telling the client that authentication is required before accessing the endpoint.
  3. The SAML2 support in HttpC detects the challenge in the received HTTP response and prepares for performing the SAML2 authentication.
  4. An embedded web browser is displayed by HttpC in which the IdP login form is displayed.
  5. The user authenticates itself to the IdP. Note that this step is completely in the hands of the IdP and is not configurable from within HttpC. Different IdPs might employ vastly different authentication mechanisms, ranging from simple username/password input forms to more complex use cases involving biometric identifications or hardware keys. All this depends on what the IdP server supports and how it is configured. This part is completely out of the hands of HttpC.
  6. As the SAML2 authentication completes, the series of redirections within the displayed embedded browser (called the web view, shortly) eventually lead back to the SP with the SAML assertion in hand. The latter is the piece of information what represents the logged-in user.
  7. The SP then establishes the authenticated session and sends a final response that the mobile client is able to detect. This will then cause the web view to disappear.
  8. With the authenticated session at hand (which is represented by HTTP cookies), HttpC can restart the conversation and retry the original request. If successful, the resource is going to be returned to the client.

The most important points of integration with a SAML2-based infrastructure can be found in these steps, namely:

  1. The Service Provider must be able to signal the SAML2 challenge in a predefined way that the client can easily detect.
  2. When the flow completes successfully, the Service Provider is required to send a final response signaling the end of the flow. This allows the mobile client to dismiss the embedded web browser and resume the original request.
  3. The Service Provider must use HTTP cookies to establish the authenticated session.

Requirements 1 and 3 follow from the SAML2 standard. However, no. 2 is something that must be implemented by the Service Provider on top of the compliance with the standard. The final response in question is actually expected to be a redirect which can be easily detected in the web view by observing the opened URL. The endpoint producing the final response is what HttpC calls the finish endpoint. This term is going to be important later.

Details of the SAML2 support

As said before, HttpC supports SAML2 Web SSO. This profile is configurable from the aspect of what bindings are used for the SAML2 request and the SAML2 response. HttpC supports the following setups:

  • HTTP POST and HTTP Redirect Binding for the SAML2 request
  • HTTP POST and HTTP Artifact Binding for the SAML2 response

HTTP Artifact Binding for the SAML2 request is not supported.

The SAML2 standard does not allow HTTP Redirect Binding for the SAML2 response.

HttpC expects a challenge when a protected resource is accessed without an authenticated session. The challenge itself is a SAML2 request that the SP sends back in the response with one of the supported bindings. Out of these, HTTP POST binding is used more frequently. As this consists of a text/html response that contains a plain-old HTML form, its detection requires HttpC to parse the output HTML and search its contents for the form to see if it contains the SAML request.

Doing this can be slow, therefore the SP can implement support for the identifying header. This is an extra HTTP response header that can be sent as part of the SAML2 challenge which makes detection a lot easier. If it's present, the client concludes that a challenge has been received. This eliminates the need for doing expensive HTML parsing.

This header is not part of the SAML2 standard. It is an SAP-specific extension to make things easier for the mobile client. SAP Mobile Platform Server and the Mobile Services on SAP Cloud Platform support this header.

SAML2 configuration

To enable SAML2 support in HttpC, a configuration must be provided that tells a HttpConversationManager how to detect the challenge and what endpoints to use in the SAML2 flow.

Parts of this configuration have already been mentioned in the previous sections. Below is the summary:

  • Identifying Header: This is the header that the SP is recommended to send along with the HTTP response containing the SAML2 request if it's configured to use HTTP POST Binding. Recall that the presence of the SAML2 request itself represents the authentication challenge. This header is optional.
  • Finish Endpoint: This is the endpoint hosted by the Service Provider which must be opened when the challenge is detected. Simply put, when the embedded browser is displayed, this endpoint is the first thing opened by it. The SP is then expected to respond with a SAML2 challenge again which starts the entire SAML2 flow inside a browser environment.
  • Finish Endpoint Parameter: This is the request parameter that is used to signal the end of the SAML2 flow. If the embedded browser opens the finish endpoint again but with this extra request parameter in the URL then the web view is dismissed and the conversation is restarted.

One might wonder why does the flow start by opening the finish endpoint? The reason is that the above described SAML2 flow is a reactive authentication mechanism: one must open the protected resource to provoke the challenge. From the aspect of the mobile client, the finish endpoint therefore acts like a special protected resource that is expected to require the same kind of authenticated session as the original resource.

A compliant Service Provider therefore should:

  • ...support the identifying header (when using HTTP POST Binding) to make it easier for the mobile client to detect a challenge,
  • ...provide a finish endpoint that can produce the same kind of SAML2 challenge that the mobile client would get when accessing other protected resources,
  • ...make sure that when the authentication to the finish endpoint succeeds, it is reopened with the extra finish endpoint parameter. The latter is usually implemented using a simple HTTP redirect.

All these features are required from the SP to make it work with the SAML2 support of HttpC.

The previously mentioned SAP server-side solutions (SAP Mobile Platform server, Mobile Services on SAPcp) already support this mechanism. When a mobile application registered in SMP or Mobile Services on SAPcp accesses a protected OData resource, the server ensures that the established session grant access only to said resource. All this happens transparently to the client.

Setting up a manager

To configure a HttpConversationManager for SAML2, again the CommonAuthenticationConfigurator class should be used. The addSAML2ConfigProvider: method must be called with an object argument whose class conforms to the SAML2ConfigProviderProtocol. Then, all conversation managers configured with this configurator will have SAML2 enabled.

The SAML2 configuration provider is a similar asynchronous user exit like the one used for Basic Authentication or Mutual SSL/TLS. The difference is that meanwhile these two providers actually produce credentials to authenticate with, the SAML2 configuration provider - as its name implies - produces only configuration.

The typical configuration for the above mentioned SAP servers is as follows:

  • Identifying Header: com.sap.cloud.security.login
  • Finish Endpoint: <server scheme, host and port>/SAMLAuthLauncher
  • Finish Endpoint Parameter: finishEndpointParam

Note that the actual values of the identifying header and the finish endpoint parameter are not relevant. Their sheer presence is enough for the SAML2 functionality to work.

If you'd like to integrate SAML2 support in your app, implement a new SAML2 configuration provider and return a new configuration by calling the completion block of the provideSAML2ConfigurationForURL:completionBlock: method.

Similarly to Mutual SSL/TLS, a configuration provider (albeit asynchronous) is recommended to avoid lengthy computations or too many UI interactions. This is because a configuration provider is invoked before every request.

Insights and summary

Read this section if you'd like to better understand the motives behind the SAML2 support and why the SAML2 flow is implemented the way it is described above. This might shed some light on the challenges supporting SAML2 on the mobile with a native solution. Furthermore, the insights contained herein might help you understand what can and what cannot be expected from this solution. Considering that SAML2 is one of the most complex auth. standards in the industry, this should prove useful.

The SAML2 support in HttpC expects that the Service Provider meet additional requirements on top of being a fully compliant SAML2 SP. This is caused by that the Web Browser SSO Profile has originally been designed for plain-old HTML form-based web applications. There, posting a HTML form leads to a full page reload. This mechanism is what SAML2 "hijacks" for the purpose of authentication which if succeeds will eventually produce the results of the original form post.

A native HTTP client solution operating inside a mobile application however has much more limitations. It is unable to participate in web-based authentication flows transparently. It needs to explicitly open an embedded browser to allow the user to log in. Consequently, knowing when to start and when to end the flow is critical.

On the other hand, embedding web views in native mobile applications on iOS has its own limitations too. Due to security considerations, a native application cannot access the actual HTML contents the web view is displaying. It can only see what is the URL of the page that the embedded web view is about to open. Hence the need for the finish endpoint parameter which can be used to easily detect the end of the flow.

These mobile client-specific peculiarities imply the need for the above extensions to a normal SAML2 authentication flow. However, the SAP Mobile Platform server and Mobile Services on SAP Cloud Platform both properly implement the necessary requirements.

It should be noted that the SAML2 IdP does not have to be customized (i.e. augmented with extra, mobile-specific features) to take part in this flow. This is because all communication with the IdP takes place inside the embedded web view which implies the following:

  • The SAML2 assertions are not intended to be exposed to the client and as such are not retrievable using HttpC.
  • The IdP login screen cannot be altered in any way. Any customizations required there must be implemented on the IdP itself.
  • The embedded web view is opened only for the sole purpose of logging the user in. An IdP login screen might offer other functions beyond simple authentication (for ex. links to Forgot Your Password? or Registration pages) but it must be ensured that eventually the flow ends in a successful login. Without it, the user might be "stuck" in the embedded web view, surfing around meanwhile a running HTTP conversation awaits for the authenticated session. To prevent this, avoid placing such distractions on the IdP screen.

OAuth2

OAuth2 is one of the most complex authorization flows supported by the HttpConvAuthFlows library. However, setting it up is relatively easy. If the mobile application uses services hosted by Mobile Services on SAPcp, getting things up and running is even easier.

SAP Mobile Platform on-premise server does not support OAuth2 at the time of writing this guide.

This section provides all the info to help you to set up a HttpConversationManager to communicate with OAuth2 protected endpoints.

It is assumed that you have a solid understanding of how OAuth2 works in general. Should there be any black spots for you, please read through some of the introductory materials on the Internet before proceeding to read this part of the guide.

Supported grant types

The OAuth2 standard defines various grant types which differ in the method how the access token is acquired. HttpC supports the following types:

  • Authorization Code Grant
  • Password Grant
  • Client Credentials Grant

Although the standard mentions it, the so-called Implicit Grant type is not supported by HttpC. A variation of Authorization Code Grant is supported instead where the client_secret parameter is optional. This follows well-known best practices.

Unquestionably, the most widely used grant type is Authorization Code Grant which is often used to implement an OAuth2-based SSO solution. Think about the various apps and services that let you register and log in using your Facebook or Google Account credentials, for example.

Extension points of the standard

Before discussing what the OAuth2 features of HttpConvAuthFlows are, let's take a closer look at the standard in terms of extension points. The below emphasized points are what the OAuth2 support in HttpC is built on top of.

The no. 1 goal of HttpC in supporting OAuth2 is to offer a reactive mechanism that can respond to a challenge presented by the server and trigger an OAuth2 authorization flow. However, the OAuth2 standard does not define what an OAuth2 challenge is. In fact, every Resource Server (in OAuth2 terms) can have its own way of asking the client for an access token. The first question to answer is therefore how to detect a challenge?

Similarly to SAML2, when a challenge is detected the OAuth2 flow must be started. The similarity does not end here: like SAML2, the OAuth2 support in HttpC also requires a configuration to know which endpoints to talk to, where to acquire the access and refresh tokens, etc..

The third thing is the scope of the access token. Remember that OAuth2 is an authorization standard and the access token is something that implicitly represents a set of resources, i.e. those that can be accessed with it. Again, the standard does not define any means to derive what endpoints the access token might be good for.

The above questions arise for all supported grant types independently. The next section provides the answers HttpC gives to all these.

The OAuth2ServerSupportProtocol protocol

Enabling OAuth2 support is done following the same logic as other auth. types. The addOAuth2ServerSupport: method of CommonAuthenticationConfigurator should be called with an object whose class conforms to the OAuth2ServerSupportProtocol. Such objects are called server support objects, hereafter.

This protocol is what enriches the generic OAuth2 mechanism in HttpC with the specifics related to a particular server. Effectively, this is what answers the questions introduced in the previous section.

Here's how a server support object is integrated into the overall OAuth2 flow:

  1. The isSupportedEndpoint:completion: method is called with the URL of the currently executing request whenever an HTTP response with status code between 400 (inclusive) and 500 (exclusive) is received. This method then can check if the URL points to a server whose OAuth2-specifics are known. If this method reports false then the current server support object is skipped entirely.

For example, a server support object tailored to SAP Cloud Platform can return true only if the URL is pointing to the expected SAPcp service endpoint. If the request is fired for any other endpoint, the SAPcp-specific server support object will not be considered competent and HttpC will look for another object. 1. A HTTP 4xx response means that the client sent an incorrect request. However, this is not necessarily an indication that an OAuth2 access token would have been required. To decide this, the canRecognizeChallengeInResponse: method is called on the server support object.

Remember: this method is called only if the server support object passed the previous step, that is, it supports the endpoint.

This method has access to the entire response through its NSURLResponse argument. It can be used to analyse the original request URL, response headers and even the payload. If the server implements a clear way of signaling the need of an access token, here this can be checked. The result of this method is an assessment about the response. If this states that the HTTP response at hand is indeed a challenge then the flow continues with the current server support, otherwise the response is not processed any further from the aspect of OAuth2. 1. At this point the HTTP 4xx response has been analysed and found that it is indeed a challenge presented by a supported endpoint. The configForChallenge:completion: method is then called to get the configuration in the form of an OAuth2Config object.

For each grant type there exists a corresponding subclass of OAuth2Config. The configForChallenge:completion: method therefore has to return an object belonging to the appropriate subclass. This is what decides what grant type is to be used.

For example, OAuth2AuthCodeGrantConfig pertains to the Authorization Code Grant type and contains properties such as the authorization endpoint, client ID and client secret, etc.. 1. The next step depends on the actual grant type. The most complex from this aspect is the Authorization Code Grant with an authorization endpoint that uses the HTTP URL scheme. This is the most common use case and causes an embedded web view to be displayed. As the web view appears, the authorization endpoint URL is loaded which triggers the first part of the authorization flow. The end of this step is the authorization code that concludes the user's direct involvement and instructs HttpC to dismiss the web view.

If you are already familiar with how the SAML2 flow works you might see the similarity here. Again, an embedded webview has to be used, however this fits in with the flow much more smoothly. This is because the OAuth2 standard explicitly mandates the use of a HTTP redirect to the redirect URI with the authorization code included as query parameter. That makes detecting the end of the flow possible without the need for additional requirements from the server side. 1. Independently of the grant type, at a certain point acquiring the access and refresh tokens becomes possible. This is all done transparently and without the involvement of the server support object. 1. Once the tokens are at hand, two things are performed: * The tokens are placed in a so-called OAuth2 token storage represented by an instance of OAuth2TokenStorageProtocol. Prior to saving the tokens, the transformRequestURLToStorageURL: method of the server support object in use is called. This is what determines the scope of said tokens. Read the next section concerning how this works exactly. * The original request is retried (i.e. the conversation gets restarted) but this time the access token is placed in the HTTP request inside the Authorization: Bearer *** header.

The built-in OAuth2 support is intelligent enough to retry previously acquired access tokens if they are found in the storage. Also, if a refresh token is also present then HttpC will try to use it first to renew the access token, should it expire. The above flow unfolds in its entirety only if none of these attempts are successful in acquiring a valid access token locally.

Scope of access and refresh tokens

As previously mentioned, the OAuth2 standard does not define in any way what kind of resources might an access token be used for. For example, the OAuth2 token acquired by a particular mobile application on SAPcp will grant access only to the OData resources exposed by the corresponding OData service hosted by Mobile Services. For other kinds of services, you'd need a different token.

A mechanism therefore is required which associates a URL with the corresponding access and refresh tokens. If this association is in place, HttpC can easily find the right token for a particular HTTP endpoint.

The association is implemented using the transformRequestURLToStorageURL: method of the server support object. It is recommended to become familiar with the API documentations of this method later on.

Here's what happens: when a freshly acquired access token/refresh token pair is to be saved, this method is called to transform the concrete request URL into a symbolic one for identification purposes. The scope of the tokens is therefore going to be represented by the transformation itself. Imagine the below transformation:

http://foo-services.com/bar/sales/** -> http://foo-services.com/bar
http://foo-services.com/bar/users/** -> http://foo-services.com/bar
http://foo-services.com/bar/products/** -> http://foo-services.com/bar

http://foo-services.com/bar/admin/** -> http://foo-services.com/bar/admin

The ** sequence denotes a subpath of arbitrary depth.

On the left are concrete URLs, on the right are symbolic ones returned by transformRequestURLToStorageURL:.

Let's say that a native request is sent to http://foo-services.com/users/johndoe?detail=name,email,phone. If this is OAuth2-protected then the corresponding server support object is called to help acquire the OAuth2 tokens. After successful authorization, this request URL is then transformed in the above way, hence producing http://foo-services.com/bar. This URL is then used as a key into a key-value storage represented by the OAuth2TokenStorageProtocol in use. Later on, when another request is fired against the same endpoint (or for another endpoint that transforms to the same symbolic URL) then the tokens can be found and used.

Note that the above example implies that an access token acquired for the users sub-service of bar is usable for the sales and products sub-services as well. However, these access tokens would not be suitable for the admin sub-service as it transforms to a different symbolic URL.

This mechanism is especially effective when sending a request after the tokens have been acquired. There, HttpC can efficiently read the tokens from the storage and try them when sending a request without waiting for an authentication challenge. This proactive behaviour is built into the library and requires no further coding.

Token storages

An OAuth2 token storage is represented by an instance of OAuth2TokenStorageProtocol. The library by default uses an in-memory storage that stores tokens in an NSDictionary. If you'd like to keep tokens around for a longer period (for ex. by storing them in the iOS KeyChain) you can implement your own token storage protocol class.

To use a custom implementation, override the tokenStorage property of CommonAuthenticationConfigurator prior to configuring a HttpConversationManager object.

Note how token storages are explicitly separated from server support objects. This is because the two things do actually represent different aspects of the OAuth2 mechanism. An app can easily make use of its own storage mechanism meanwhile using server support implementations from other parties.

The storage is used at the end of a successful OAuth2 flow to store the tokens and during request sending to retrieve the access token to be sent along. For the details on how to implement a storage, read the API documentation of OAuth2TokenStorageProtocol.

Enabling support for SAPcp

The library ships the SAPOAuth2ServerSupport class that is an implementation of OAuth2ServerSupportProtocol tailored to SAP Cloud Platform. To make use of it, instantiate an object of this type and pass it to the addOAuth2ServerSupport: method of the common configurator.

This object requires that the server URL, client ID and secret, scope and redirect URI be specified during initialization. This will drive its internal logic to know which endpoint is to be supported, how to detect the challenge and what the scope of the acquired access/refresh tokens is.

At the time of writing this guide, SAP Cloud Platform supports the Authorization Code Grant and Client Credential Grant types but SAPOAuth2ServerSupport produces OAuth2 configuration only for the former.

The above parameters can be taken directly from the admin UI of the Mobile Services cockpit. The minimum configuration requires only the server URL and the client ID. If the client secret is left empty and the OAuth2 endpoints are not customized in any way then SAPOAuth2ServerSupport can make use of the default values it derives internally for the rest of its parameters. In the majority of the cases this should be enough.

Summary

The below things are worth remembering about the OAuth2 support of HttpC:

  • What server we are trying to communicate with? How does it handle OAuth2? The answer is provided by a OAuth2ServerSupportProtocol object that must be added to the common configurator. This knows what endpoints it supports, how to detect a challenge, what parameters are required to initiate the authorization flow and what the scope of the acquired tokens is going to be.
  • How should we store the tokens? To configure this, a OAuth2TokenStorageProtocol must be implemented and set in the corresponding property of the common configurator. Remember, this means that you're going to be in charge of storing the tokens HttpC acquires for you.
  • For SAP Cloud Platform servers, the SAPOAuth2ServerSupport is the most elegant solution. It's a class that should be instantiated with the proper parameters taken from the Mobile Services Cockpit. This should be enough to allow the mobile application to work with our OData endpoints hosted in the cloud.

Two-Factor Authentication

Two-Factor Authentication (also known as: One-Time Pin Authentication) is an additional authentication step supported by both SAP Mobile Platform server and Mobile Services on SAP Cloud Platform. It can be enabled on the administration cockpit at the security settings page. Consult the documentation of these products for the related info.

Enabling 2FA on the admin cockpit is not enough. The end user must install the SAP Authenticator mobile application and enable second factor authentication on his/her account. This must be done on the user administration page of the IdP.

The IdP must be the same that the SMP server or the SAP Cloud Platform account is integrated with, of course.

This guide does not intend to go into the details of setting up the infrastructure. Please find the related materials on the SAP Help Portal.

Overview

2FA usually takes place after successfully passing the first authentication stage. This can be anything that HttpC supports: Basic Authentication, Mutual SSL/TLS, etc.. When the second stage begins, an embedded web view is opened where a one-time passcode is asked from the user. This passcode can be obtained by opening the SAP Authenticator application preinstalled on the same device. Once the second factor authentication succeeds, the user is logged in.

At this point it is strongly recommended to have a solid understanding of how SAML2 authentication works with HttpC. Please jump to this section if you are not yet familiar with the topic. 2FA shares a lot of common traits with how SAML2 is implemented.

Just like SAML2 and (in most cases) OAuth2, Two-Factor Authentication also involves opening a web view. Here's how the flow takes place after the first authentication stage:

  1. The need for the second-factor authentication is signaled by the server using an HTTP redirect. Along with the redirect response a special response header is sent. The client can use this information to realize that this is a special redirect and that 2FA is to be commenced.
  2. The redirect points to the One-Time Passcode form. HttpC - instead of following this redirect with a native request as part of the running conversation - loads this URL in a newly opened embedded web view.
  3. The OTP form loads and presents the user with the one-time pin challenge.
  4. The user opens the SAP Authenticator app to obtain the pin code.

    At the time of writing this guide, this is possible in two ways: 1. The user taps on the link that navigates to the appropriate screen of SAP Authenticator. 1. The user opens SAP Authenticator manually, navigates to the proper screen that generates the One-Time Pin for the current application and then copy-pastes the passcode into the OTP form.

  5. Once the challenge is answered successfully, the server redirects to an URL that contains the finish endpoint parameters. This is very similar to the same-named concept of SAML2 and is used by the mobile client to detect the end of the flow in the web view.

At the end of the above process the running conversation is restarted and the request is retried. The authenticated state is captured by the HTTP cookies the server issues at the end.

OTP configuration

Don't be confused: the terms 2FA and OTP are often used interchangeably. They refer to the same thing.

The OTP configuration is supplied via an instance of OTPConfigProviderProtocol. Like other authentication methods, multiple instances of this provider may be registered with a CommonAuthenticationConfigurator by calling the addOTPConfigProvider: method.

The configuration consists of the following attributes:

  • Response Header Name: The name of the response header that must be looked for when receiving a HTTP 3xx response.
  • Response Header Value: If a HTTP 3xx redirect is received which has a response header whose name equals to the previous attribute and its value equals to this attribute then HttpC considers this response a 2FA challenge.
  • OTP Additional URL Parameters: Additional URL parameters to append to the OTP form URL before opening it in a web view. This attribute is optional.
  • Finish Endpoint Parameters: The list of URL parameters to look for when the web view opens a new page. Once the 2FA process completes successfully, the server appends these parameters to the URL it redirects to. The client uses it to detect the end of the flow.

The default configuration for SAP Mobile Platform server and Mobile Services on SAPcp is as follows:

  • Response Header Name: x-smp-authentication
  • Response Header Value: otp-challenge
  • OTP Additional URL Parameters: redirecttooriginalurl=false
  • Finish Endpoint Parameters: finished=true

Communication between SAP Authenticator and your app

The OTP form contains a link that can be used to open the SAP Authenticator application conveniently. If the user taps it, Authenticator is opened which generates the pin code and then calls back to your application.

The mechanism behind this communication is based on custom URI schemes. The above mentioned link has sapauthenticator as its URI scheme. The opened URL contains several parameters which helps the Authenticator identify which application is calling it.

As part of setting up SAP Authenticator, your application has to be registered with it which means adding the callback URL pointing to your application to the user's account. By convention, this URL should have a custom URI scheme that ends with .xcallbackurl.

Such URLs are usually quite lengthy. To facilitate registration, SAP Authenticator provides a QR code reader using which one can easily add the URL of your app.

To make this work, you have to register this callback URL in your Info.plist file. Now, when SAP Authenticator is called, it will be able to call back to your registered application with the passcode in the URL.

How to properly set up SAP Authenticator and the entire 2FA landscape is out of the scope of this guide. The above discussion relies on that you have a solid understanding of how it works. You can find useful material about this topic on the SAP Help Portal.

The last missing piece to integrate in your app is what detects this URL, extracts the passcode and channels it into the embedded web view where it gets submitted to the server for validation. This last piece is implemented using the CustomURLSchemeManagement class. Here's how you integrate it:

#import "AppDelegate.h"
#import "CustomURLSchemeManagement.h"

@implementation AppDelegate

...

-(BOOL)application:(UIApplication*)app
       openURL:(NSURL*)url
       options:(NSDictionary<UIApplicationOpenURLOptionsKey,id>*)options {
    [[CustomURLSchemeManagement sharedInstance] handleOpenURL:url];

    return YES;
}

@end

The iOS platform calls the UIApplicationDelegate of your application when someone tries to open a URL that has a custom URI scheme registered by your app. The above code snippet passes this URL to HttpC. Without the above integration, the link on the OTP form will not work properly. Still, SAP Authenticator will reopen your app but the OTP form in the web view will not be informed of the passcode and will just wait there as if nothing happened.

Of course, in the above case the user is still able to open SAP Authenticator and copy-paste the passcode to the OTP form manually.

It is highly recommended to read this section about how custom URI schemes are handled by HttpC.

Summary

  • Two-Factor Authentication is an additional authentication step taking place after the first-stage authentication is successful. It presents an additional challenge to the user which can be answered using a one-time passcode generated by a properly configured SAP Authenticator application running on the same device.
  • The mechanism 2FA uses is rather similar to how SAML2 support works in HttpC. It's also relying on an embedded web browser that opens the OTP form presenting the challenge.
  • To enable, an OTP configuration provider must be implemented and registered with the common configurator. This will then enrich the configured HttpConversationManager with the ability to detect and orchestrate the 2FA flow.
  • For the communication between SAP Authenticator and your app to work nicely, the custom URI schemes and the integration of CustomURLSchemeManagement have to be set up correctly.
  • The configuration is pretty much static for all kinds of SAP Mobile Platform server and Mobile Services on SAPcp endpoints.

Deep dive

This section is dedicated to readers wishing to explore the more complex features of the HttpConversation library. Reading this part is not crucial in getting started with it. However, if you'd like to use more advanced features and/or extend the library in a way not covered by the previous sections, read on...

The conversation context

Before jumping into writing your own (request, response, etc..) filters an important concept called the conversation context should be understood first.

Technically, an HTTP conversation may be composed of multiple HTTP requests. This is especially true if any of the authentication methods are configured. If a server responds with a challenge, HttpC needs to handle it and then retry the request. Sometimes there might be multiple retries before the correct credentials are found and/or acquired.

This implies that a conversation itself has some internal state which spans over multiple requests. This is what the OAuth2 support uses internally to keep track of whether new or existing tokens were tried. Again, this is what the SAML2 support uses to prevent infinite loops if the server responds with a challenge even though the user has already logged in. The latter can happen if the server is not configured properly.

The conversation context is the object that maintains this state. Its lifecycle starts when the first request of a particular conversation is started and ends when the last request finishes.

The context is represented by the HttpConversationContextProtocol that is part of the HttpConversation library. This protocol is implemented by built-in classes of the NSURLSession API via Objective-C categories. Namely:

  • NSHTTPURLResponse
  • NSURLAuthenticationChallenge
  • NSMutableURLRequest
  • NSURLSessionTask

These objects appear as arguments in many places in the HttpC API and the URL Session API. You can find them in block-based methods, NSURLSession delegate protocols, etc..

To access the related methods, import the <class name>+HttpConversation.h header. For URL responses for example, this header is going to be NSHTTPURLResponse+HttpConversation.h.

The most important elements of the conversation context are accessible via its properties:

  • conversationId: A unique identifier of a conversation. The various execute* methods of HttpConversationManager each return the identifier of the started conversation. If there's need for it, you can use this returned value to match it with the one stored in a conversation.
  • contextMap: This is a dictionary of arbitrary key-value pairs. Whatever value you set in this dictionary will survive conversation restarts but will not live past the end of the conversation. This is what HttpConvAuthFlows itself makes use of internally to keep track of the internal state of various authentication methods.

It's recommended to read the API documentations of the above mentioned Objective-C categories and the context protocol itself. These cover the remaining details that should prove valuable before you start using this part of the API.

Writing custom filters

If you want to write your own filters then you must implement the appropriate protocol in your own class, instantiate the filter and then register it with a HttpConversationManager instance using the corresponding add*Filter: method.

The API documentations of the filter protocols already clearly describe what contracts should be honored by the implementations. Nonetheless, some of the most important contracts are emphasized below:

  • Make sure filter implementations are stateless, that is, they do not store conversation-specific information in ivars or properties. The stateless nature is required to ensure overall thread-safety. Remember, a conversation manager might run multiple conversations in parallel, each of which will use your filter. If you keep internal state at an inappropriate location (i.e. in ivars) then you're going to have a problem real soon which is rather difficult to debug.
  • If you are in need of associating some temporary information with the running conversation, use the conversation context. Via the various Objective-C categories, the context is available through one of the arguments of your filter method. Read this section if you have not done already.
  • Avoid maintaining a reference to the HttpConversationManager to which your filter is registered. Doing so would cause circular references which can rather easily lead to resource leaks.

Implementing a filter is the right thing to do if you'd like to enrich many different kinds of requests in your application with a certain functionality. It's kind of like a component that always takes part of request sending no matter the targeted endpoint.

Filters are used quite extensively by HttpConvAuthFlows to support the above documented auth. methods. In fact, all these methods are based purely on filters. There's no hidden API that HttpConvAuthFlows makes use of. You could implement your own SAML2 or OAuth2 request and response filters, if you want.

A typical example for a use case that fits filters is, for example, CSRF protection. Servers that protect against cross-site request forgery attacks usually ask for an extra CSRF token as part of every data modification (i.e. POST/PUT/DELETE/etc..) request. Depending on how this is implemented by the server, acquiring and sending this token is something that can be condensed into a filter implementation of your own. Then, all the requests you send using HttpC can make use of this mechanism transparently.

Data task → Download task transformation

The NSURLSessionDataDelegate has a special method: URLSession:dataTask:didBecomeDownloadTask:. It is invoked when a request that originally starts out as a data task is turned into a download task.

Such a thing can happen if during the data request it turns out that the payload is rather large. The URL Session API allows for changing the task type on the fly. Here's how it is done:

  1. The URLSession:dataTask:didReceiveResponse:completionHandler: method of the NSURLSessionDataDelegate is called. If this method decides that the transformation must take place then the handler is called with the NSURLSessionResponseBecomeDownload content disposition.
  2. URL Session will perform the conversion and call URLSession:dataTask:didBecomeDownloadTask:.

Please observe: neither NSURLSession nor HttpConversationManager will command the transformation on its own. This is something that you must command explicitly as part of your delegate implementations.

With HttpC, the above sequence of delegate method calls takes place the same way. However, it has its consequences:

  • Internally, the running HTTP conversation will no longer be associated with a data task but with a download task instead. However, the conversation identifier (returned by the original execute* method that started everything) will not change.
  • The above conversion is possible only if you use the delegate-based APIs: you must implement a delegate that commands the conversion in the URLSession:dataTask:didReceiveResponse:completionHandler: method. If you do so, make sure you use the delegate to process the result of the download, just like if you used plain URL Session API.
  • If the conversation was started with a block-based API together with a delegate that commands the conversion the block will still be called but the response will belong to the result of the download task.

The conversion can take place independently of the running authentication mechanisms.

This is a rather seldom used feature of URL Session API, probably because it requires quite some coding and housekeeping to get right. We recommend you avoid this type of conversion for simplicity's sake. If there's a chance that the HTTP endpoint you're trying to access might reply with a very large payload, try to send a preflight HEAD request to determine the content length.

OAuth2 token management

During OAuth2 authorization, OAuth2ServerSupportProtocol objects are used to orchestrate the flow and a OAuth2TokenStorageProtocol object is used to store the tokens.

However, one might need to manage the tokens independently of the running conversations. This is where the OAuth2 token manager, represented by the OAuth2TokenManager class, comes into play.

The CommonAuthenticationConfigurator has a property called tokenManager that can be used to access the manager. By nature, the common configurator instance is a short-lived object that is used only for the time of configuring some HttpConversationManager instances. However, the token manager is something that can and should be kept around for a longer period.

The key thing is this: the token manager provides another entrance into dealing with OAuth2 access and refresh tokens. It can be used to delete some or all tokens, retrieve tokens for a particular URL, etc.. Here's how you use it:

  1. Configure OAuth2, as instructed by this section, on a common configurator instance.
  2. Optionally, set a token storage on the same configurator instance.
  3. Configure one or more HttpConversationManagers with it. These will have OAuth2 support enabled.
  4. Get the token manager from the common configurator and put it away for later use.
  5. At this point, the common configurator instance can be thrown away.

The token manager put away in step no. 4 will have access to the same server support objects and the same token storage as the conversations started by the managers configured in step no. 3.

Suppose you're accessing SAP Cloud Platform with our built-in server support SAPOAuth2ServerSupport and your application-specific token storage. If you then put away the token manager you can use it to drive a logout logic, for example. Simply call the appropriate method on the manager to remove the tokens for a particular URL. Internally, the manager will then make sure the same tokens are wiped which were acquired using SAPOAuth2ServerSupport and your storage implementation.

Most authentication and authorization methods are based on HTTP cookies. However, OAuth2 is using access and refresh tokens. The OAuth2 token manager therefore implements a similar access to such tokens as a cookie storage provides access to actual cookie values.

It is highly recommended to read the API documentation of the tokenManager property of CommonAuthenticationConfigurator and the OAuth2TokenManager class. It contains some additional details and insights you might find useful.

Custom URI scheme management

iOS native applications have the ability to register custom URI schemes (like. com.example.myapp) by adding the corresponding entries in the Info.plist file. Whenever another application opens a URL that has this scheme, the registered application is started and the application:openURL:options: method of its UIApplicationDelegate is called.

The 2FA and OAuth2 features of HttpC might rely on custom URI schemes. For the former, registering a custom scheme is required to complete the integration with SAP Authenticator. For OAuth2, custom URI schemes might be used in the authorization endpoint URL and/or the redirect URI to implement non-web-based authorization flows.

It's recommended to look through the API documentations of the OAuth2Config class and its subclasses. They contain some additional info concerning custom URI schemes you should consider before implementing such a flow.

The HttpConvAuthFlows library defines a singleton, called CustomURLSchemeManagement that should be integrated into your application as follows:

#import "AppDelegate.h"
#import "CustomURLSchemeManagement.h"

@implementation AppDelegate

...

-(BOOL)application:(UIApplication*)app
       openURL:(NSURL*)url
       options:(NSDictionary<UIApplicationOpenURLOptionsKey,id>*)options {
    [[CustomURLSchemeManagement sharedInstance] handleOpenURL:url];

    return YES;
}

@end

What happens is that when your app gets opened with a custom URL it is immediately passed over to the URL scheme management class of HttpC. Internally, it then distributes it to various handlers which are waiting for certain URLs to be opened. For example, in case of Two-Factor Authentication the internal URL handler registered with CustomURLSchemeManagement will listen for the URL that SAP Authenticator opens after it generates the passcode.

From the aspect of HttpC, CustomURLSchemeManagement acts as a dispatcher which, if properly integrated the above way, helps make custom URI scheme-based mechanisms work. If you intend to implement your own filters which communicate with external apps using custom URLs, consider implementing your own CustomURLSchemeHandler and registering it with the URL scheme management singleton. As always, check the API documentations of these classes for further info.

Web strategies

SAML2, OAuth2 and Two-Factor Authentication are features of HttpC which often rely on opening an embedded web browser. Normally, HttpC does this by displaying an instance of UIWebView in a separate UIWindow.

The HttpConvAuthFlows library however allows for customizing these web-based flows. Customization here means that you can replace UIWebView with your own web view implementation.

All web-based flows are implemented via web strategies. Each of the three listed features have a corresponding web strategy protocol. These are:

  • SAML2WebStrategy
  • OAuth2WebStrategy
  • OTPWebStrategy

Albeit these are different protocols, all of them follow the same philosophy. Whenever HttpC needs to display a web view it calls the appropriate strategy implementation. Each of these protocols define a single main method which gets everything that's needed to display a web view as arguments and returns the results via a completion block.

For example, OAuth2WebStrategy defines the startAuthorizationCodeFlowWith:completion: method that gets the OAuth2 authorization code grant configuration as argument and produces the authorization code via the completion block.

Consequently, if you want to integrate your own web view solution you must implement the corresponding strategy and register it with the WebStrategies singleton via a resolver object. For each strategy protocol a corresponding resolver protocol belongs whose sole responsibility is to create and return a strategy for certain arguments. Example:

#import "WebStrategies.h"
#import "OAuth2AuthCodeGrantConfig.h"

/**
 The custom OAuth2 web strategy that displays a custom web view.
 */
@interface MyOAuth2Strategy <OAuth2WebStrategy> @end
@implementation MyOAuth2Strategy

-(void)startAuthorizationCodeFlowWith:(OAuth2AuthCodeGrantConfig*)config
       completion:(void (^)(NSString*))completionBlock {
    // Display the custom web view and make sure to call the completion block
    // once the web view loads the redirect URI containing the authorization code.
    ...
}

@end

/**
 The resolver pertaining to 'MyOAuth2Strategy' making sure the custom strategy is used
 only with certain OAuth2 configurations and hosts.
 */
@interface MyOAuth2StrategyResolver <OAuth2WebStrategyResolver> @end
@implementation MyOAuth2StrategyResolver

-(id<OAuth2WebStrategy>)resolveForConfiguration:(OAuth2Config*)config {
    if ([config isKindOfClass:[OAuth2AuthCodeGrantConfig class]]) {
        OAuth2AuthCodeGrantConfig* acgConfig = (OAuth2AuthCodeGrantConfig*) config;
        if ([acgConfig.authorizationEndpoint.host isEqualToString:@"myservice.com"]) {
            return [MyOAuth2Strategy new];
        }
    }

    return nil;
}

@end

void configure() {
    [[WebStrategies sharedInstance] registerOAuth2Resolver:[MyOAuth2StrategyResolver new]];

    ...
}

When calling the configure() function, our custom OAuth2 strategy resolver gets registered with the WebStrategies singleton. Looking at the implementation of MyOAuth2StrategyResolver we can see that this resolver will return a new instance of MyOAuth2Strategy if the configuration is for the Authorization Code Grant type and the authorization endpoint is hosted by myservice.com. In any other case nil is returned.

If the registered resolvers (no matter the type) are not able to produce custom strategy objects then WebStrategies always falls back to using the default. Therefore, the above code will use the custom web view implementation integrated via MyOAuth2Strategy only when an OAuth2 authorization code flow is to be started using an authorization endpoint that's hosted by myservice.com. For any other configuration the built-in UIWebView-based solution will be used.

Customizing web views is seldom required. In most cases the UIWebView-based solution works just fine. However, there have been advancements lately on the iOS platform with respect to embeddable web view components. Furthermore, there are hybrid frameworks (like Apache Cordova) which are running most of the code in embedded browsers. Being able to plug in these solutions via custom web strategies makes HttpC even more flexible.