Request, Response, Challenge filters in HttpConversationManager
The HttpConversationManager filters
When we were starting the design for our next-generation network component, the HttpConversationManager, we looked extensively at simply reusing and extending the two common iOS networking components: namely, NSURLSession, and AFNetworking. We’re very conscious of the work in the AFNetworking community, building on NSURLConnection and NSOperation, opening side-modules for OAuth, web-sockets, implementing pinning, etc. Also, the modern NSURLSession APIs that came with iOS7, with the “data task” concept, file transfer, background activity, block interfaces, integrated Kerberos, gave us confidence that most enterprise-grade connectivity cases could be implemented with high-level native API’s. This was really our goal: if iOS, Android, and Windows OS’s provided enterprise-grade API’s, reuse them!
The limitation we hit on both of these options was native support for SAML2, which is critical in many enterprise landscapes, and for SAP’s HANA Cloud Platform identity provider. Also, we commonly have unique requirements around custom SSO tokens, etc., which need to be ordered in the HTTP request procedure. So, taking some ideas from the open-source community, we implemented a very thin wrapper on NSURLSession with NSURLSessionDataTask, which is now the HttpConversationManager.
I’ve written some here about some of the concepts in HttpConversationManager. What I’d like to share in this post is an explanation of the three filter types that can be added to the HttpConversationManager, to do pre- and post- request processing, and handle authentication challenges.
For most cases, no pre/post-processing is required, so you can support Basic and Client Certificate authentication by adding a single filter (Challenge). SAML2 authentication is a post-processing operation, and requires a single Response filter. OAuth2 requires both a Request and Response filter. SAP has already implemented these: as a developer, you just need to set them to your HttpConversationManager. For custom SSO procedures, you can order your custom filter(s) around the regular authentication types as necessary.
Filters on a HttpConversationManager
In short, there are three filter types: the RequestFilter, ResponseFilter, and ChallengeFilter.
- The RequestFilter is processed before the request is sent over the network, and can modify the request. Also, the original request can be paused, while a 2nd operation is completed (like obtaining a token from a 3rd-party service, then re-started when the RequestFilter is complete.
- The ChallengeFilter is processed next in the HTTP request, if the server responds with a HTTP authentication challenge. When a ChallengeFilter is executed, it should return a credential in response to the challenge. This is identical to supplying a credential in the NSURLSession
- The ResponseFilter is processed last in the HTTP request flow, after the response is complete. Like the RequestFilter, it allows you to inspect the response payload, and complete additional operations, before passing the finished response to the app. Unlike the RequestFilter, you cannot modify the payload of the response.
Request flow, from Application to Service Provider
Delegate method called before executing the request.
@param mutableRequest: the request instance which will be executed
@param conversationManager: copy of HttpConversationManager instance, can be used for starting additional requests
@param completionBlock: call the block when the filter finished the modification of <i>mutableRequest</i> object
-(void) prepareRequest:(NSMutableURLRequest*)mutableRequest conversationManager:(HttpConversationManager*)conversationManager completionBlock:(void (^)())completionBlock;
The interface for creating a new request on HttpConversationManager
-(void) executeRequest:(NSMutableURLRequest*)urlRequest completionHandler:(void (^)(NSData* data, NSURLResponse* response, NSError* error))completionHandler
takes a NSMutableURLRequest as an input parameter. The RequestFilter may touch that mutableRequest instance, to add headers, etc.
-prepareRequest: method also passes a copy of the HttpConversationmanager, which “can be used for starting additional requests”. This is important, if, for instance, you were using OAuth2, and knew that each request must include a Bearer auth token. If the auth token isn’t available, you need to call an endpoint on the IdP in order to get it–before sending the original request to its destination. Otherwise, the original request will fail with a 403. So, you could use the
conversationManager instance to execute this secondary request.
Using a copy of the original HttpConversationManager for these types of secondary requests is convenient, but not required. The convenience factor comes from the fact that your remaining Request, Challenge, and Response filters are still attached to the
conversationManager instance, so you don’t need to create a new conversation, configure it, etc. However, it is important to know that the
conversationManager instance variable is a copy of the original HttpConversationManager, and that the completed and in-progress RequestFilters have been removed from the instance’s
requestFilter array. So, you don’t need to worry about the infinite loop problem, but you should think for a moment to check that your secondary request on this
conversationManager instance doesn’t expect any discarded RequestFilters.
In general, this should never be a problem. If a complex case existed which required all the original filters for a secondary request, you could simply initialize a new HttpConversationManager, configured specifically for this request.
Once the NSURLMutableRequest is completely prepared by the RequestFilter, then you should invoke the void
completionBlock(). The next RequestFilter in the HttpConversationManager will then be executed, according to the order of the HttpConversationManger
Delegate method called when authentication challenge occurs.
@param challenge: NSURLAuthenticationChallenge
@param conversationManager: copy of HttpConversationManager instance, can be used for starting additional requests
@param completionBlock: call the block when the filter finished its job. <br> Return YES, and a NSURLCredential object, if the challenge is handled, or return NO and nil, if the challenge is not handled. If return YES and NSURLCredential object is nil, it will be handled as no credential provided.
-(void) handleChallenge:(NSURLAuthenticationChallenge*)challenge conversationManager:(HttpConversationManager*)conversationManager completionBlock:(void (^)(BOOL useCredential, NSURLCredential* credential))completionBlock;
The ChallengeFilter is *only* executed when the NSURL loading system invokes the handler of NSURLAuthenticationChallenge. Inside the HttpConversationManager, after the RequestFilters are all completed, a NSURLSessionDataTask is created from the NSMutableURLRequest, and sent to the server. When the NSURLSessionDelegate method
– (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
is invoked, the array of `challengeFilters` is enumerated.
The SAP SDK ships with standard ChallengeFilters for UsernamePassword (Basic) and ClientCert. When they are invoked, they check the
protectionSpace.authenticationMethod value of the NSURLAuthenticationChallenge, to see if they should try to supply a credential. For example, if the
authenticationMethod == NSURLAuthenticationMethodClientCertificate, the ClientCertChallengeFilter will try to supply a credential.
The way that the ChallengeFilters find the credentials to supply is through an array of attached Providers. A Provider is a protocol which a developer can implement, with their own customizations to match their particular landscape, MDM/MAP provider, SCEP provider, etc. These can be as simple as just returning a hard-coded username/password, showing a secure-style UIAlertView, or more complex: calling a 3rd-party API to get a client certificate, showing a custom UI, etc.
The simplest version is a UsernamePasswordProviderProtocol, which can be as easy as just returning a NSURLCredential with username & password from code.
– (void)provideUsernamePasswordForAuthChallenge:(NSURLAuthenticationChallenge *)authChallenge completionBlock:(void (^)(NSURLCredential *, NSError *))completionBlock
NSURLCredential *credential = [[NSURLCredential alloc] initWithUser:@”myName”
The ClientCertProviderProtocol has a similar signature: after obtaining the client certificate from the keychain, DataVault, 3rd-party API, etc., pass it into the completion block as a NSURLCredential:
– (void) provideClientCertForAuthChallenge:(NSURLAuthenticationChallenge*)authChallenge completionBlock:(void (^)(NSURLCredential*, NSError*))completionBlock
// obtain the certificate(s) and construct SecIdentityRef
NSURLCredential *cred = [[NSURLCredential alloc] initWithIdentity:mySecIdentityRef
For the standard Basic auth & client certificate auth cases, I’d recommend just using the ChallengeFilters out-of-the box from the SDK, implementing the Provider to supply the user credentials.
If you’re implementing your own ChallengeFilter (this is ok, even for the standard types, so long as you substitute your filters for the standard ones on the HttpConversationManager, or order them in-front of the standard ones) then you might not bother with using the Provider protocol–you might just supply the credential directly from within the filter. In this case, make sure that the NSURLCredential is passed into the
// filter code, and obtain the NSURLCredential
completionBlock(BOOL useCredential, NSURLCredential* credential);
The response filter behaves exactly like the request filter, except it is executed on a successful NSURL response, and the contents of the response are not mutable.
The developer has access to the NSURLResponse and NSData payload, and can execute custom read procedures before calling the completion block. The primary use case for the response filter is trapping response payloads related to authentication operations. To this end, the method signature contains a parameter named
shouldRestartRequest, which should be set to YES in the event that the response contains an authentication token, or an authentication redirect (as in the SAML2 case), and the original requests can now be successfully authenticated.
In general, the response filter should not be used for handling the response payload, as the developer expects to get the successful payload in the completion block of his request. But, the response filters are executed before payloads are passed to the parser under the SODataStore, so they can be an effective way to trap error responses, especially HTML content, from a server–before it ends up in the OData parser.
In summary, the HTTPConversationManager’s Request- and ResponseFilters give you the ability to touch requests from the application before they go to the server, and handle the responses from the server before they’re returned to the application. The ChallengeFilters give you the ability to respond to an NSURLAuthenticationChallenge, in the same way you normally would within the NSURLSessionDelegate. To implement your own filters, conform to the protocol, and add them to the array on the conversation Manager.