I am implementing my own authentication framework for OAuth 2.0.
As far as my understanding is concerned server sends 401 if token has been expired.
I implemented NSURLConnection's delegate
- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;
to catch these error and refresh token.
- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge{
// refresh token and send it to server
if ([challenge previousFailureCount] > 0) {
// do something may be alert message
}
else
{
//refreshToken
}
}
But it seems to be that there is no way I can append the token to url.
you can't - changing the url means a new request. the current request is in progress and it is for this URL.
one straightforward way is to have a 'wrapper object' around the url connection itself, that could transparently do a 2nd request should the 1st fail
e.g. PSEUDOCODE
#interface MyConnection
- loadRequest:(NSURLRequest*)r completionHandler:handler //not fully felshed out for this example
#end
#implementation MyConnection
- loadRequest:(NSURLRequest*)r completionHandler:handler //not fully felshed out for this example
{
[NSURLConnection sendRequest:r completion:{
if(response.status == 401)
{
NSMutableURLRequest *r2 = [NSMutableURLRequest requestWithURL:r.URL + token];
//refresh by sending a new request r2
[NSURLConnection sendRequest:r2 completion:{
handler(response);
}];
}
else {
handler(response);
}
}];
}
#end
Related
I am currently serving videos in my iOS application with MPMoviePlayerController. The files are streamed from our backend server that requires authentication. It is key-based authenticated set in the Authorization HTTP Header.
It used to work perfectly with single video files. Now we’re trying to implement HLS adaptive streaming and we have faced a wall. I am currently using a custom NSURLProtocol subclass to catch requests made to our backend server and inject the proper Authorization header. For HLS it simply doesn’t work.
When we looked at the server logs, we clearly saw that the first request to the m3u8 file worked fine. Then all subsequent calls made (other m3u8 files and ts also) are 403 forbidden. It seems that MPMoviePlayerController doesn’t use NSURLProtocol for the other files. (side note: It does work on the simulator thought, but not on a physical device which let me think that both are not implemented in the same way).
MPMoviePlayerController instantiation
self.videoController = [[MPMoviePlayerController alloc] initWithContentURL:video.videoURL];
The URL Protocol interception
+ (NSURLRequest *) canonicalRequestForRequest:(NSURLRequest *)request {
NSMutableURLRequest *newRequest = request.mutableCopy;
[newRequest setValue:#"HIDDEN" forHTTPHeaderField:#"Authorization"];
return newRequest;
}
Any Ideas, suggestions, work arounds?
After verification with Apple Developer Technical Support, I figured what I wanted to achieve is impossible (and unsupported).
Here's the quote from the reply :
The problem you're seeing with NSURLProtocol and so on is that the movie playback subsystem does not run its HTTP requests within your process. Rather, these requests are run from within a separate system process, mediaserverd. Thus, all your efforts to affect the behaviour of that playback are futile.
By using NSURLProtocol, you can intercept the communication between MPMoviePlayerController and the streamed requests. To inject cookies along the way, or possibly save the stream offline videos. To do this, you should to create a new class extending NSURLProtocol:
Hope this helps you:
GAURLProtocol.h
#import <Foundation/Foundation.h>
#interface GAURLProtocol : NSURLProtocol
+ (void) register;
+ (void) injectURL:(NSString*) urlString cookie:(NSString*)cookie;
#end
GAURLProtocol.m
#import "GAURLProtocol.h"
#interface GAURLProtocol() <NSURLConnectionDelegate> {
NSMutableURLRequest* myRequest;
NSURLConnection * connection;
}
#end
static NSString* injectedURL = nil;
static NSString* myCookie = nil;
#implementation GAURLProtocol
+ (void) register
{
[NSURLProtocol registerClass:[self class]];
}
// public static function to call when injecting a cookie
+ (void) injectURL:(NSString*) urlString cookie:(NSString*)cookie
{
injectedURL = urlString;
myCookie = cookie;
}
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
if([[[request allHTTPHeaderFields] objectForKey:#"Heeehey"] isEqualToString:#"Huuu"])
{
return NO;
}
return [[[request URL] absoluteString] isEqualToString:injectedURL];
}
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
return request;
}
// intercept the request and handle it yourself
- (id)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id<NSURLProtocolClient>)client {
if (self = [super initWithRequest:request cachedResponse:cachedResponse client:client]) {
myRequest = request.mutableCopy;
[myRequest setValue:#"Huuu" forHTTPHeaderField:#"Heeehey"]; // add your own signature to the request
}
return self;
}
// load the request
- (void)startLoading {
// inject your cookie
[myRequest setValue:myCookie forHTTPHeaderField:#"Cookie"];
connection = [[NSURLConnection alloc] initWithRequest:myRequest delegate:self];
}
// overload didReceive data
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[[self client] URLProtocol:self didLoadData:data];
}
// overload didReceiveResponse
- (void)connection:(NSURLConnection*)connection didReceiveResponse:(NSURLResponse *)response {
[[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:[myRequest cachePolicy]];
}
// overload didFinishLoading
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
[[self client] URLProtocolDidFinishLoading:self];
}
// overload didFail
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
[[self client] URLProtocol:self didFailWithError:error];
}
// handle load cancelation
- (void)stopLoading {
[connection cancel];
}
#end
Register
// register protocol
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[NSURLProtocol registerClass:[GAURLProtocol class]];
return YES;
}
Usage
[GAURLProtocol injectURL:#"http://example.com/video.mp4" cookie:#"cookie=f23r3121"];
MPMoviePlayerController * moviePlayer = [[MPMoviePlayerController alloc]initWithContentURL:#"http://example.com/video.mp4"];
[moviePlayer play];
#Marc-Alexandre Bérubé I can think of below workaround:
Run a proxy server in your app to proxy all the video URL's. Download all the video content by injecting the necessary auth headers to the request and relay back the content via the proxy server to the media player to render it. This approach may not work for large videos as the video rendering would only start after entire video is downloaded.
I recently started to work with NSURLConnection in my project and I'm wondering whether or not the pattern I use to handle the received data is appropriate.
In case I get a 404 or another error, I do not actually want to do anything with the data, so it would be a waste to still append it to my object. Therefore I only want to create the data object once I get a 200 status.
Is it safe to assume that -connection:didReceiveResponse: is called before any of the -connection:didReceiveData: callbacks?
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
[self.data appendData:data];
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSHTTPURLResponse *)response
{
if (response.statusCode == 200) {
self.data = [NSMutableData data];
}
else {
NSLog(#"Connection failed with status code %d", response.statusCode);
[self.connection cancel];
}
}
Yes
didReceiveResponse will call beforeDidReceiveData, and it is possible it get calls many time in one connection as per apple document
You should be prepared for your delegate to receive the
connection:didReceiveResponse: message multiple times for a single
connection; this can happen if the response is in multipart MIME
encoding. Each time the delegate receives the
connection:didReceiveResponse: message, it should reset any progress
indication and discard all previously received data (except in the
case of multipart responses).
Source
I'm communicating with a server that validates a password and returns a 401 error on invalid password, together with a json body specifying the number of failed attempts. That number is incremented by the server on each failed validation.
The problem I'm facing is that when NSURLConnection gets a 401 response, it kicks an authentication mechanism that involves these delegate methods:
connection:canAuthenticateAgainstProtectionSpace:
connection:didReceiveAuthenticationChallenge:
If I return NO in the canAuthenticate method, a new identical request will be made. This will result in the server incrementing the failed attempts a second time (which is obviously not desired) and I'll get a 401 response (connection:didReceiveResponse:)
If I return YES in the canAuthenticate method, then the didReceiveAuthenticationChallenge method is called. If I want to stop the second request, I can call [challenge.sender cancelAuthenticationChallenge:challenge]. But if I do that, I won't get a 401 response, but an error.
I've found no way to capture the first 401 response. Is there any way to do that?
1) For plain vanilla SSL without client certificate you don't need to implement these 2 methods
2) If you still want to, you should check for the HTTP response code in the [challenge failureResponse] object:
- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
NSURLCredential *urlCredential = [challenge proposedCredential];
NSURLResponse *response = [challenge failureResponse];
int httpStatusCode = -1;
if(response != nil) {
NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response;
httpStatusCode = [httpResponse statusCode];
}
if(urlCredential != nil || httpStatusCode == 401) {
//wrong username or more precisely password, call this to create 401 error
[[challenge sender] cancelAuthenticationChallenge:challenge];
}
else {
//go ahead, load SSL client certificate or do other things to proceed
}
}
- (BOOL)connection:(NSURLConnection *)connection canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace *)protectionSpace
{
return YES;
}
If all else fails, try this: a wonderful library available, called AFNetworking, which is very easy to implement.
It uses blocks, which greatly simply communication of data between classes (does away with delegates), and is asynchronous.
Example usage is below:
AFHTTPClient *client = [[AFHTTPClient alloc] initWithBaseURL:[NSURL URLWithString:"www.yourwebsite.com/api"]];
NSDictionary *params = #{
#"position": [NSString stringWithFormat:#"%g", position]
};
[client postPath:#"/api" parameters:params success:^(AFHTTPRequestOperation *operation, id responseObject) {
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
}];
As simple as that! Result is available directly within the class that calls the HTTP Post or Get method.
It even includes image and JSON requests, JSON deserialization, file download with progress callback, and so much more.
In my first ViewController (MonitorViewController) this is in the interface file MonitorViewController.h:
#import <RestKit/RestKit.h>
#interface MonitorViewController : UIViewController <RKRequestDelegate>
In MonitorViewController.m ViewDidLoad method, I have this at the end:
RKClient* client = [RKClient clientWithBaseURL:#"http://192.168.2.3:8000/DataRecorder/ExternalControl"];
NSLog(#"I am your RKClient singleton : %#", [RKClient sharedClient]);
[client get:#"/json/get_Signals" delegate:self];
The implementation of delegate methods in MonitorViewController.m:
- (void) request: (RKRequest *) request didLoadResponse: (RKResponse *) response {
if ([request isGET]) {
NSLog (#"Retrieved : %#", [response bodyAsString]);
}
}
- (void) request:(RKRequest *)request didFailLoadWithError:(NSError *)error
{
NSLog (#"Retrieved an error");
}
- (void) requestDidTimeout:(RKRequest *)request
{
NSLog(#"Did receive timeout");
}
- (void) request:(RKRequest *)request didReceivedData:(NSInteger)bytesReceived totalBytesReceived:(NSInteger)totalBytesReceived totalBytesExectedToReceive:(NSInteger)totalBytesExpectedToReceive
{
NSLog(#"Did receive data");
}
My AppDelegate method DidFinishLaunchingWithOptions method only returns YES and nothing else.
I recommend using RestKit framework. With restkit, you simply do:
// create the parameters dictionary for the params that you want to send with the request
NSDictionary* paramsDictionary = [NSDictionary dictionaryWithObjectsAndKeys: #"00003",#"SignalId", nil];
// send your request
RKRequest* req = [client post:#"your/resource/path" params:paramsDictionary delegate:self];
// set the userData property, it can be any object
[req setUserData:#"SignalId = 00003"];
And then, in the delegate method:
- (void)request:(RKRequest *)request didLoadResponse:(RKResponse *)response {
// check which request is responsible for the response
// to achieve this, you can do two things
// check the parameters of the request like this
NSLog(#"%#", [request URL]); // this will print your request url with the parameters
// something like http://myamazingrestservice.org/resource/path?SignalId=00003
// the second option will work if your request is not a GET request
NSLog(#"%#", request.params); // this will print paramsDictionary
// or you can get it from userData if you decide to go this way
NSString* myData = [request userData];
NSLog(#"%#", myData); // this will log "SignalId = 00003" in the debugger console
}
So you will never need to send the parameters that are not used on the server side, just to distinguish your requests. Additionally, the RKRequest class has lots of other properties that you can use to check which request corresponds to the given response. But if you send a bunch of identical requests, I think the userData is the best solution.
RestKit will also help you with other common rest interface tasks.
I would like to support HTTP Basic Authentication in my UIWebView.
At the moment, I am canceling requests in
webView:shouldStartLoadWithRequest:navigationType: then handle them in my own NSURLConnectionDelegate to check for and provide credentials if needed. I then use loadData:MIMEType:textEncodingName:baseURL: to present HTML in the web view. That works fine for any URLs that are passed to the delegate.
My problem is that the delegate is never called for embedded elements, like images, JavaScript or CSS files. So if I have an HTML page which references an image which is protected with basic authentication, that image cannot be loaded properly. Additionally, webView:didFinishLoad: is never called, because the web view could not fully load the page.
I have checked that case with Terra, a third-party browser available on the App Store, and it can fully cope with that situation. I think it would be possible to solve this by providing my own NSURLProtocol, but that seems too complicated. What am I missing?
Try to use sharedCredentialStorage for all domains you need to authenticate.
Here is working sample for UIWebView it was tested against Windows IIS having only BasicAuthentication enabled
This is how to add your site credentials:
NSString* login = #"MYDOMAIN\\myname";
NSURLCredential *credential = [NSURLCredential credentialWithUser:login
password:#"mypassword"
persistence:NSURLCredentialPersistenceForSession];
NSURLProtectionSpace *protectionSpace = [[NSURLProtectionSpace alloc]
initWithHost:#"myhost"
port:80
protocol:#"http"
realm:#"myhost" // check your web site settigns or log messages of didReceiveAuthenticationChallenge
authenticationMethod:NSURLAuthenticationMethodDefault];
[[NSURLCredentialStorage sharedCredentialStorage] setDefaultCredential:credential
forProtectionSpace:protectionSpace];
[protectionSpace release];
Edit: same code in Swift 4
let login = "MYDOMAIN\\myname"
let credential = URLCredential(user:login, password:"mypassword", persistence:.forSession)
let protectionSpace = URLProtectionSpace(host:"myhost", port:80, protocol:"http", realm:"myhost", authenticationMethod:NSURLAuthenticationMethodDefault)
URLCredentialStorage.shared.setDefaultCredential(credential, for:protectionSpace)
Your webView is supposed to work now, if it does not work use next code to debug, especially check log messages of didReceiveAuthenticationChallenge.
#import "TheSplitAppDelegate.h"
#import "RootViewController.h"
#implementation TheSplitAppDelegate
#synthesize window = _window;
#synthesize splitViewController = _splitViewController;
#synthesize rootViewController = _rootViewController;
#synthesize detailViewController = _detailViewController;
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// Override point for customization after application launch.
// Add the split view controller's view to the window and display.
self.window.rootViewController = self.splitViewController;
[self.window makeKeyAndVisible];
NSLog(#"CONNECTION: Add credentials");
NSString* login = #"MYDOMAIN\\myname";
NSURLCredential *credential = [NSURLCredential credentialWithUser:login
password:#"mypassword"
persistence:NSURLCredentialPersistenceForSession];
NSURLProtectionSpace *protectionSpace = [[NSURLProtectionSpace alloc]
initWithHost:#"myhost"
port:80
protocol:#"http"
realm:#"myhost" // check your web site settigns or log messages of didReceiveAuthenticationChallenge
authenticationMethod:NSURLAuthenticationMethodDefault];
[[NSURLCredentialStorage sharedCredentialStorage] setDefaultCredential:credential forProtectionSpace:protectionSpace];
[protectionSpace release];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:#"http://myhost/index.html"]
cachePolicy:NSURLRequestReloadIgnoringCacheData
timeoutInterval:12
];
NSLog(#"CONNECTION: Run request");
[[NSURLConnection alloc] initWithRequest:request delegate:self];
return YES;
}
- (void)applicationWillResignActive:(UIApplication *)application
{
}
- (void)applicationDidEnterBackground:(UIApplication *)application
{
}
- (void)applicationWillEnterForeground:(UIApplication *)application
{
}
- (void)applicationDidBecomeActive:(UIApplication *)application
{
}
- (void)applicationWillTerminate:(UIApplication *)application
{
}
- (void)dealloc
{
[_window release];
[_splitViewController release];
[_rootViewController release];
[_detailViewController release];
[super dealloc];
}
- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;
{
NSLog(#"CONNECTION: got auth challange");
NSString* message = [NSString stringWithFormat:#"CONNECTION: cred cout = %i", [[[NSURLCredentialStorage sharedCredentialStorage] allCredentials] count]];
NSLog(message);
NSLog([connection description]);
NSLog([NSString stringWithFormat:#"CONNECTION: host = %#", [[challenge protectionSpace] host]]);
NSLog([NSString stringWithFormat:#"CONNECTION: port = %i", [[challenge protectionSpace] port]]);
NSLog([NSString stringWithFormat:#"CONNECTION: protocol = %#", [[challenge protectionSpace] protocol]]);
NSLog([NSString stringWithFormat:#"CONNECTION: realm = %#", [[challenge protectionSpace] realm]]);
NSLog([NSString stringWithFormat:#"CONNECTION: authenticationMethod = %#", [[challenge protectionSpace] authenticationMethod]]);
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{
// release the connection, and the data object
[connection release];
// inform the user
NSLog(#"CONNECTION: failed! Error - %# %#",
[error localizedDescription],
[[error userInfo] objectForKey:NSURLErrorFailingURLStringErrorKey]);
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;
{
NSLog(#"CONNECTION: received response via nsurlconnection");
}
- (BOOL)connectionShouldUseCredentialStorage:(NSURLConnection *)connection;
{
NSLog(#"CONNECTION: USE!");
return YES;
}
#end
The final solution for WebView authentication was based on custom protocol implementation. All protocols registered as a stack, so if you redefine HTTP protocol it would intercept all requests coming from webView, so you have to check attributes assotiated with incoming request and repack it into new request and send it again via your own connection. Since you are in stack, your request immidiatly comes to you again and you have to ignore it. So it goes down protocol stack to real HTTP protocol implementation, since your request is not athenticated you'll get authenticaiton request. And after authenticaiton you'll get a real response from server, so you repack response and reply to original request received from webView and that's it.
Don;t try to create new requests or responses bodies, you have to just resend them. The final code would be aproximetly 30-40 lines of code and it is quite simple, but requires a lot of debuging and tetsing.
Unfortunatlly I cannot provide code here, since I am assigned to different project already, I just wanted to say that my post is wrong way, it stucks when user changes password.
The secret to HTTP basic authentication using cocoa is knowing NSURL and the related classes.
NSURL
NSURLRequest/NSMutableURLRequest
NSURLConnection
NSURLCredential
NSURLCredentialStorage
NSURLProtectionSpace
UIWebView/WebView/NIWebController etc.
The real magic comes from NSURLConnection. In the words of the devDocs, "An NSURLConnection object provides support to perform the loading of a URL request." If you want to load some a URL in the background without displaying it you would use NSURLConnection. The real power of the NSURLConnection is in the method
+ (NSURLConnection *)connectionWithRequest:(NSURLRequest *)request delegate:(id < NSURLConnectionDelegate >)delegate
The NSURLConnectionDelegate protocol has methods for responding to successful connections, fatal errors, and authentication challenges. If you are trying to access data Protected by HTTP basic authentication this is how Cocoa does it. At this point an example should bring some clarity.
//basic HTTP authentication
NSURL *url = [NSURL URLWithString: urlString];
NSMutableURLRequest *request;
request = [NSMutableURLRequest requestWithURL:url
cachePolicy:NSURLRequestReloadIgnoringCacheData
timeoutInterval:12];
[self.webView openRequest:request];
(void)[NSURLConnection connectionWithRequest:request delegate:self];
This creates a URL. From the URL a URLRequest is created. The URLRequest is then loaded in the web view. The Request is also used to make a URLConnection. We don't really use the connection, but we need to receive notifications about authentication so we set the delegate. There are only two methods we need from the delegate.
- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;
{
NSURLCredential * cred = [NSURLCredential credentialWithUser:#"username"
password:#"password"
persistence:NSURLCredentialPersistenceForSession];
[[NSURLCredentialStorage sharedCredentialStorage]setCredential:cred forProtectionSpace:[challenge protectionSpace]];
}
- (BOOL)connectionShouldUseCredentialStorage:(NSURLConnection *)connection;
{
return YES;
}
Whenever there is an authentication challenge a credential is added to the credential storage. You also tell the connection to use the credential storage.
I've just implemented this by setting basic auth credentials using an NSMutableURLRequest for the UIWebView. This also avoids the round trip incurred when implementing sharedCredentialStorage (of course there are tradeoffs involved).
Solution:
NSString *url = #"http://www.my-url-which-requires-basic-auth.io"
NSString *authStr = [NSString stringWithFormat:#"%#:%#", username, password];
NSData *authData = [authStr dataUsingEncoding:NSASCIIStringEncoding];
NSString *authValue = [NSString stringWithFormat:#"Basic %#", [authData base64EncodedString]];
NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:url]];
[mutableRequest setValue:authValue forHTTPHeaderField:#"Authorization"];
NSURLRequest *request = [mutableRequest copy];
NSURLRequest *request = [NSURLRequest basicAuthHTTPURLRequestForUrl:url];
[self.webView loadRequest:request];
You can grab the NSData+Base64 category which implements the base64EncodedString for NSData from Matt Gallagher's page (it was at the bottom of the blog post when I downloaded it)
For TKAURLProtocolPro [http://kadao.dir.bg/cocoa.htm]
For SVWebViewController [https://github.com/samvermette/SVWebViewController]
Make sure to remember that logging out is not so easy with sessions and UIWebView credentials. See answer here: https://stackoverflow.com/a/18143902/2116338.