iOS canAuthenticateAgainstProtectionSpace method not called every time - ios

I am trying to perform SSL certificate validation and have implemented the delegate canAuthenticateAgainstProtectionSpace
- (BOOL)connection:(NSURLConnection *)connection canAuthenticateAgainstProtectionSpace: (NSURLProtectionSpace*)protectionSpace
{
OSStatus status = SecTrustEvaluate(protectionSpace.serverTrust, &trustResult);
if(status == errSecSuccess)
{
}
else
{
}
}
However, I notice that this delegate gets called the first time for a given URL, but not for subsequent attempts for the same URL. I thought this had to do with the cached response , so I created the NSURLRequest like the following:
NSURLRequest *request = [[NSURLRequest alloc]
initWithURL: [NSURL URLWithString:_urlString]
cachePolicy: NSURLRequestReloadIgnoringLocalAndRemoteCacheData
timeoutInterval: 10
];
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
This doesn't help either. Any ideas, how I can get canAuthenticateAgainstProtectionSpace method to get called every time?

The answer above doesn't solve the actual problem. The actual problem here is that an authentication challenge is only being presented the first time a connection is established for that URL while the app is open.
As explained here
A TLS session is processor intensive and Apple doesn't want you to create a new one every time a connection is made to that URL, so they cache one for you. In this case, it's working against you, but you should be able to work around the issue by including a "." character at the end of your host.
In our case, we were trying to establish a connection to a web server containing a certificate issued by an in-house CA. Since we knew the CA wouldn't be trusted on the first connection, we allowed the connection to continue so that the CA could be downloaded. During that connection, we add the "." character to the end of the host. All subsequent connections use the regular URL without the "." character at the end of the host. This ensures that the CA cert we downloaded is validated the first time a "real" connection is made.

I solved the problem by adding the following code:
- (void)connection:(NSURLConnection *)connection
didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
[[challenge sender] cancelAuthenticationChallenge:challenge];
}
The above cancels the authentication challenge and so the delegate canAuthenticateAgainstProtectionSpace gets called every time

Related

Why do i get authenticationchallenge with NSURLRequest when switching to HTTPS?

I've used NSMutableURLRequest for a long time to connect to my server.
In order to avoid double roadtrips, i set the usr/pwd right away in the header, like this:
NSMutableURLRequest *request = [NSMutableURLRequest
requestWithURL:[NSURL URLWithString:url]
cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:HTTP_REQUEST_TIMEOUT];
NSString *authStr = [NSString stringWithFormat:#"%#:%#", inUsr, inPwd];
NSString *authValue = [NSString stringWithFormat:#"Basic %#", [[authStr dataUsingEncoding:NSISOLatin1StringEncoding] base64EncodedStringWithOptions:0]];
[request setValue:authValue forHTTPHeaderField:#"Authorization"];
This has worked fine, the "willSendRequestForAuthenticationChallenge" is never called unless there is some error, so that method has always looked like:
- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
NSDictionary *errorInfo = ((NSHTTPURLResponse *) challenge.failureResponse).allHeaderFields;
NSError *error; = [NSError errorWithDomain:#"httprequesthandler" code:WRONG_CREDENTIALS userInfo:errorInfo];
[delegate finishedWithErrors:error];
Now however, i'm using the same URL's as always, only "https" instead of "http", and suddenly this method is called every time.
I want my request to work as per normal, i.e. populate basic header and only one request to the server.
I'm not sure what i'm missing, so pointers would be much appreciated!
Using https as your scheme (or protocol) requests the connection be made securely, both by encrypting the data that is transferred as well as offering some information to you about the authenticity of the server you are connecting to.
The delegate method being invoked here (connection:willSendRequestForAuthenticationChallenge:), is not related to you authenticating yourself with the server, but the server authenticating itself with you. If you dig into the challenge object (NSURLAuthenticationChallenge), you can find the credentials the server is offering to let you know that it is the server you were actually trying to connect to, instead of an impostor.
Normally you don't need to use this method unless you want to validate the server in a way that goes beyond what the OS is doing for already.

The certificate for this server is invalid

I know that if I use following nsurlconnectiondelegate it will be fixed
– connection:willSendRequestForAuthenticationChallenge: –
connection:canAuthenticateAgainstProtectionSpace
But I am trying to use
sendAsynchronousRequest:queue:completionHandler:
So you don't get the callback. I looked into apple docs it say following
If authentication is required in order to download the request, the required credentials must be specified as part of the URL. If authentication fails, or credentials are missing, the connection will attempt to continue without credentials.
I could not figure out how to do that. When I looked up all I got is this private call
+(void)setAllowsAnyHTTPSCertificate:(BOOL)inAllow forHost:(NSString *)inHost;
Any idea how to do this?
Following is the error I get
The certificate for this server is invalid. You might be connecting to
a server that is pretending to be “example.com=0x8b34da0
{NSErrorFailingURLStringKey=https://example.com/test/,
NSLocalizedRecoverySuggestion=Would you like to connect to the server
anyway?, NSErrorFailingURLKey=https://example.com/test/,
NSLocalizedDescription=The certificate for this server is invalid. You
might be connecting to a server that is pretending to be “example.com”
which could put your confidential information at risk.,
NSUnderlyingError=0xa26c1c0 "The certificate for this server is
invalid. You might be connecting to a server that is pretending to be
“example.com” which could put your confidential information at risk.",
NSURLErrorFailingURLPeerTrustErrorKey=
The webserver which you are using is asking for Server Trust Authentication, you need to properly respond with the appropriate action. You need to implement connection:willSendRequestForAuthenticationChallenge: delegate method and use SecTrustRef to authenticate it.
More information can be found here:-
https://developer.apple.com/library/ios/technotes/tn2232/_index.html
This was my code to fix error:
- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
NSURLProtectionSpace *protectionSpace = [challenge protectionSpace];
id<NSURLAuthenticationChallengeSender> sender = [challenge sender];
if ([[protectionSpace authenticationMethod] isEqualToString:NSURLAuthenticationMethodServerTrust])
{
SecTrustRef trust = [[challenge protectionSpace] serverTrust];
NSURLCredential *credential = [[NSURLCredential alloc] initWithTrust:trust];
[sender useCredential:credential forAuthenticationChallenge:challenge];
}
else
{
[sender performDefaultHandlingForAuthenticationChallenge:challenge];
}
}
Try this.
Initiate your session using custom session config as shown below:
let session = URLSession(configuration: URLSessionConfiguration.ephemeral,
delegate: self,
delegateQueue: nil)
Implement the following delegate callback method:
public func urlSession(_: URLSession, task _: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: #escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
guard let serverTrust = challenge.protectionSpace.serverTrust else {
return completionHandler(URLSession.AuthChallengeDisposition.useCredential, nil)
}
return completionHandler(URLSession.AuthChallengeDisposition.useCredential, URLCredential(trust: serverTrust))
}
If you are using AFNetworking, you can use this code:
(Just as a temp client-side solution!)
AFHTTPSessionManager * apiManager = [AFHTTPSessionManager initWithBaseURL:[NSURL URLWithString:baseURL];
AFSecurityPolicy *policy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeNone];
policy.allowInvalidCertificates = YES;
apiManager.securityPolicy = policy;
you can't fix it with the way you are trying
either drop to CFNetworking to allow bad certs
use NSConnection with a delegate and an undoc'd method
use the private API you found
all not good. CFNetwork would have to be OK for apple for now but the other 2 methods aren't even appstore-safe
Better get the server fixed. Thats the easiest and CLEANEST
This issue cannot be fixed with the way you are trying with blocks. you need to set delegates and implement the authentication challenge delegates to bypass the certificate validation.
Best solution is to either create a right certificate (make sure it is not self-signed) or change the protocol to HTTP if you are fine with it.
In my case, this error occurred due to my firewall blocked the required url. it's worked fine after removing firewall restrictions
That is a certificate error. you need to change your settings so that your program/os ignores the certificate, or add the url/certificate to a trusted list.
Sorry that is authentication, certificate is authentication. I took a look, and I found this article.
Not sure if it will resolve your issue, but it basically states, that they don't cover the case of connecting to a site with how a certificate in the documentation.
http://www.cocoanetics.com/2009/11/ignoring-certificate-errors-on-nsurlrequest/
In my case, this error occurred due to my system date. It was set as an old date, and the certificate is not effective from that old date. After correct the date, it works.

iOS HTTPS requests 101

NSURLConnection/CFURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9813)
Very, very frustrating! I've been pulling my hair for hours with this. I'm using a self-signed certificate on my Linode server. The port is 8000, couldn't get it to work on 443. I don't believe this is the reason though. Here's my code, it's 99% boilerplate:
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:#"https://www.myserver.com:8000/test.json"]];
NSURLConnection *conn = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:YES];
At the bottom:
#pragma mark NSURLConnectionDelegate
- (BOOL)connection:(NSURLConnection *)connection canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace *)protectionSpace {
NSLog(#"protectionSpace: %#", [protectionSpace authenticationMethod]);
// We only know how to handle NTLM authentication.
if([[protectionSpace authenticationMethod] isEqualToString:NSURLAuthenticationMethodNTLM])
return YES;
// Explicitly reject ServerTrust. This is occasionally sent by IIS.
if([[protectionSpace authenticationMethod] isEqualToString:NSURLAuthenticationMethodServerTrust])
return NO;
return NO;
}
- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
[[challenge sender] continueWithoutCredentialForAuthenticationChallenge:challenge];
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
NSLog(#"%#", response);
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
NSLog(#"%#", data);
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
NSLog(#"didFailWithError");
NSLog([NSString stringWithFormat:#"Connection failed: %#", [error description]]);
}
OMG HELP!
UPDATE
It worked with this delegate method. I'm receiving the response, but there is a problem.
- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
[[challenge sender] useCredential:[NSURLCredential
credentialWithUser:#"user"
password:#"password"
persistence:NSURLCredentialPersistencePermanent] forAuthenticationChallenge:challenge];
}
The "user" and "password" that I have provided are completely random and aren't checked by the server. How can I verify the credentials before accepting the connection on my server?
EDIT: I'm running a Node.js server
Getting the corresponding error description may help:
So, first the error domain kCFStreamErrorDomainSSL means that the error code is an SSL error code as defined in Security/SecureTransport.h:
kCFStreamErrorDomainSSL, -9813 means:
errSSLNoRootCert = -9813, /* cert chain not verified by root */
And that simply means, you have no trusted root certificate and the connection fails because of that authentication failure.
Provide a root certificate on the device for the server trust authentication and you are fine.
There are a few approaches to implement server trust authentication with self-signed certificates, the one more secure than the other.
The simplest approach requires a self-signed certificate which is stored in the bundle of the app, then retrieved and simply byte-compared. Here is an example:
Implementing server trust authentication with a self-signed certificate.
These are a must read also: Technical Note TN2232
HTTPS Server Trust Evaluation and Technical Q&A QA1360 Describing the kSecTrustResultUnspecified error.
The more preferred approach is to use a CA (Certificate Authority) which you can be yourself. That is, you create your own CA and your certificates signed with this CA.
The steps are similar:
Bundel the DER file of your CA's root certificate in your app.
Handle the server trust authentication as follows:
get the authentication challenge
retrieve the trust object from the challenge
create a certificate object from the data in your bundle
set the certificate object as an anchor to the trust object using function SecTrustSetAnchorCertificates.
evaluate the trust
not sure if this will actually fix the problem, but it may help. you should be using
– connection:willSendRequestForAuthenticationChallenge:
since the other methods are deprecated. take a look at the overview of the NSURLConnectionDelegate protocol

NSURLRequest: How to handle a redirected post?

I have a tried and tested use of NSURLRequest (and accompaniments) implementation, that works great for GETs, and POSTs for a given URL.
However, I want to now move the target of the URL without changing the URL used by the app, so I'm intending to use a webhop redirect via my DNS provider.
This works fine for the GET requests, but the POSTs just hang... no connection response is received.
The relevant iOS method for handling a redirect is,
-(NSURLRequest *)connection:(NSURLConnection *)connection
willSendRequest:(NSURLRequest *)request
redirectResponse:(NSURLResponse *)redirectResponse
According to Apple's documentation for (handling redirects),
If the delegate doesn't implement connection:willSendRequest:redirectResponse:, all canonical changes and server redirects are allowed.
Well, that's not my experience, because leaving this method out does not work for me. The request just hangs without a response.
Apple also suggests an implementation, of willSendRequest (see above linked Apple documentation), again this doesn't work for me. I see the invocations, but the resulting requests just hang.
My current implementation of willSendRequest is as follows (see below). This follows the redirect, but the handles the request as if it was a GET, rather than a POST.
I believe the problem is that the redirection is losing the fact that the HTTP request is a POST (there may be more problems, such as carrying the request Body forward too?).
I'm not sure what I should be doing here. So any advice on how to correctly handle a POST that receives a redirect would be appreciated. Thanks.
-(NSURLRequest *)connection:(NSURLConnection *)connection
willSendRequest:(NSURLRequest *)request
redirectResponse:(NSURLResponse *)redirectResponse
{
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *) redirectResponse;
int statusCode = [httpResponse statusCode];
NSLog (#"HTTP status %d", statusCode);
// http statuscodes between 300 & 400 is a redirect ...
if (httpResponse && statusCode >= 300 && statusCode < 400)
{
NSLog(#"willSendRequest (from %# to %#)", redirectResponse.URL, request.URL);
}
if (redirectResponse)
{
NSMutableURLRequest *newRequest = [request mutableCopy]; // original request
[newRequest setURL: [request URL]];
NSLog (#"redirected");
return newRequest;
}
else
{
NSLog (#"original");
return request;
}
}
ADDITIONAL INFORMATION 1
The HTTP code received by willSendRequest is 301 - 'Moved Permanently.
Using allHTTPHeaderFields to extract the header fields, I see that he request I originally submit has the header
HTTP header {
"Content-Length" = 244;
"Content-Type" = "application/json";
}
...and the copied / redirected request has the header,
Redirect HTTP header {
Accept = "*/*";
"Accept-Encoding" = "gzip, deflate";
"Accept-Language" = "en-us";
"Content-Type" = "application/json";
}
...which doesn't look like a copy of the original request, or even a superset.
Keep your original request, then provide your own willSendRequest:redirectResponse: to customize that request, rather than working with the one Apple provides you.
- (NSURLRequest *)connection: (NSURLConnection *)connection
willSendRequest: (NSURLRequest *)request
redirectResponse: (NSURLResponse *)redirectResponse;
{
if (redirectResponse) {
// The request you initialized the connection with should be kept as
// _originalRequest.
// Instead of trying to merge the pieces of _originalRequest into Cocoa
// touch's proposed redirect request, we make a mutable copy of the
// original request, change the URL to match that of the proposed
// request, and return it as the request to use.
//
NSMutableURLRequest *r = [_originalRequest mutableCopy];
[r setURL: [request URL]];
return r;
} else {
return request;
}
}
By doing this, you're explicitly ignoring some aspects of the HTTP spec: Redirects should generally be turned into GET requests (depending on the HTTP status code). But in practice, this behaviour will serve you better when POSTing from an iOS application.
See also:
iOS Developer Library, URL Loading System Programming Guide: Handling Redirects and Other Request Changes
The HTTP specification for handling the 3xx class of status codes is very unfriendly towards protocols other than GET and HEAD. It expects some kind of user interaction for at the intermediary step of the redirection, which has lead to a plethora of incompatible client and server implementations, as well as a serious headache for web service developers.
From an iOS NSURL point of view, one of the things you might want to verify is that the original POST body is included in the new, redirect request.
Based off your comments on my original answer, and the edits to your question, it would appear that the URL you are trying to access has been updated permanently (301 status code). In which case you can actually avoid the redirects altogether by using the new URL.

What is the easiest way to make an HTTP GET request with IOS 5?

I am trying to send a suggestion from my app to a php file on my web server, I have tested the php script in my browser which sends an email to the user and stores the suggestion in my database, which all works fine.. And when running the following script I get a successful connection via IOS however i do not receive the results in my database..
NSString *post = [NSString stringWithFormat:#"http://blahblah.com/suggest.php?s=%#&n=%#&e=%#", suggestion, name, email];
// Create the request.
NSURLRequest *theRequest=[NSURLRequest requestWithURL:[NSURL URLWithString:post]
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:60.0];
// create the connection with the request
// and start loading the data
NSURLConnection *theConnection=[[NSURLConnection alloc] initWithRequest:theRequest delegate:self];
if (theConnection) {
NSLog(#"Connection establisted successfully");
} else {
NSLog(#"Connection failed.");
}
I have checked all the strings and encoded all spaces with %20 etc.. Can anyone see any glaringly obvious reason why my script won't work?
What is the easiest way to make a HTTP request from my app without opening safari?
You problem is that you're creating the connection, but are not sending the actual "connect" request. Instead of
NSURLConnection *theConnection=[[NSURLConnection alloc] initWithRequest:theRequest delegate:self];
try using this piece of code:
NSURLResponse* response = nil;
NSData* data = [NSURLConnection sendSynchronousRequest:urlRequest returningResponse:&response error:nil]
This is quick and dirty solution, but keep in mind that while this connection is in progress, your UI thread will appear to be frozen. The way around it is to use asynchronous connection method, which is a bit more complicated than the above. Search web for NSURLConnection send asynchronous request - the answer is there.

Resources