didReceive Challenge Authentication Method Not Called - ios

Following is my code . I am trying to use didReceive challenge method for authentication. Apple documents says that If a session task requires authentication, and there are no valid credentials available, then 'didReceive challenge' method is called. but in this case it is not being called. Any advice will be appreciated. Thanks :)
func getServerResponse(){
var request=URLRequest(url: URL(string: "http://dev.example.com/Api/Account")!)
let configuration=URLSessionConfiguration.default
request.httpMethod="GET"
let task=URLSession.init(configuration: configuration).dataTask(with: request, completionHandler: {(data,response,error) -> Void in
do {
if let jsonResult = try JSONSerialization.jsonObject(with: data!, options: []) as? NSDictionary {
print("Result-->\(jsonResult)")
print((response as! HTTPURLResponse).statusCode)
}
} catch let error as NSError {
print(error.localizedDescription)
}
})
task.resume()
}
func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: #escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
let crdential = URLCredential.init(user:"userName", password: "password", persistence: URLCredential.Persistence.none)
completionHandler(URLSession.AuthChallengeDisposition.useCredential, crdential)
}

The delegate is not being called because you have not set the delegate in the first place......
Use this method:
let task = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
Also, set the URLSessionDataDelegate and conform the protocol:
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: #escaping (URLSession.ResponseDisposition) -> Swift.Void)
Instead of using dataTask with completion handler because when you use that method it does not call any of the delegate methods.

Would like to supplement the existing answer by mentioning that Apple provide an excellent article on this. The article includes sample code. See Handling an Authentication Challenge.
Also, to mention, Apple - in both in their docs and developer forums - strongly recommend the delegate approach to handling basic authentication challenges. However! it’s worth noting that not all REST API’s issue a challenge. For example, some API’s provide a default level of access to anonymous users. They do not issue the challenge.
So, having implemented delegates correctly, if the callbacks are not executing, it’s worth checking the REST API documents on basic auth.
It maybe that there is no choice but to delegate handling and pass the appropriate headers instead:
var request = URLRequest(url: url)
request.setValue(basicAuthHeader, forHTTPHeaderField: "Authorization”)
and to provide the headers:
// Set the security header
private var credentials: String {
return "\(username):\(password)"
}
private var basicAuthHeader: String {
let data = credentials.data(using: String.Encoding.utf8)!
let encoded = data.base64EncodedString()
return "Basic \(encoded)"
}

Related

Alamofire 4.9.1 sessionManager.delegate.sessionDidReceiveChallenge is not getting assigned in iOS 15

The iOS app I'm working on is using Alamofire 4.9.1 and the following code executes without any issues in iOS 14 and below, but not iOS 15.
dataProvider.sessionDelegate.sessionDidReceiveChallenge = { _, challenge in
print("CHALLENGE ACCEPTED")
if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodClientCertificate {
return (URLSession.AuthChallengeDisposition.useCredential,
cert.urlCredential())
}
if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
return (URLSession.AuthChallengeDisposition.useCredential,
URLCredential(trust: challenge.protectionSpace.serverTrust!));
}
return (URLSession.AuthChallengeDisposition.performDefaultHandling,
Optional.none)
}
, where cert is a .pfx certificate initialized just before this.
This is preventing the app from accessing information on a server with TLS 1.2 certificate based authentication. In iOS 13 and 14 (supported iOS versions start at 13) the print statement executes, but not in iOS 15 for some reason. In iOS 13 and 14, in Alamofire's SessionDelegate.swift,
open func urlSession(
_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: #escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
gets called, but in iOS 15 that is replaced by a call to
open func urlSession(
_ session: URLSession,
task: URLSessionTask,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: #escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
Any idea as to what can be causing this and how to address it? Thank you in advance.
Interesting that this may have changed, as the documented behavior hasn't: NSURLAuthenticationMethodServerTrust challenges should be received by the session's delegate, not the task's, unless the session delegate doesn't implement the method at all. I would report this issue to Apple so they can either fix the documentation or the delegate callback itself.
In any event, Alamofire 4 is unsupported and this issue will not be fixed. Please update to Alamofire 5, which rectifies this issue by implementing only the URLSessionTaskDelegate version of this method and provides a single hook for this logic.
I managed to solve the issue by modifying the SessionDelegate.swift file of the Alamofire pod itself. Namely, I initialized the certificate in the SessionDelegate.swift file and passed its urlCredential property to the task delegate's credential property in
open func urlSession(
_ session: URLSession,
task: URLSessionTask,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: #escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
and voilà, it worked.
So I re-defined the class (PKCS12) that lets me initialize the certificate inside the SessionDelegate.swift file and the body of the above function now looks like this:
if let taskDidReceiveChallenge = taskDidReceiveChallenge {
let result = taskDidReceiveChallenge(session, task, challenge)
completionHandler(result.0, result.1)
} else if let delegate = self[task]?.delegate {
// this gets executed in iOS 15 and we need to get a credential in order for the delegate to successfully set the disposition required for getting data from the server
let cert = PKCS12.init(mainBundleResource: "\(certNameHere)",
resourceType: "pfx",
password: "%^&^%*&")
delegate.credential = cert.urlCredential()
delegate.urlSession(
session,
task: task,
didReceive: challenge,
completionHandler: completionHandler
)
} else {
urlSession(session, didReceive: challenge, completionHandler: completionHandler)
}
This has solved the issue for us and I hope it is of help to others who may experience similar issues with iOS 15 and Alamofire 4.9.1. Thank you for reading.
I have readed the Alamofire document here.
Something like that:
let rootQueue = DispatchQueue(label: "org.alamofire.customQueue")
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
queue.underlyingQueue = rootQueue
let delegate = SessionDelegate()
let configuration = URLSessionConfiguration.af.default
let urlSession = URLSession(configuration: configuration,
delegate: self,
delegateQueue: queue)
let session = Session(session: urlSession, delegate: delegate, rootQueue: rootQueue)
and you can trust the server certificate throw URLSessionDelegate.
Don't forget set delegate for it.

URLSession Credentials Caching Allowing Authentication with Incorrect Credentials

I am trying to communicate with my company's API in my iOS app. I am using the standard URLSession.
The API will load balance and redirect to a different server automatically, so I've implemented the URLSessionDelegate and URLSessionTaskDelegate methods which handle the redirects.
When I initially login I will get redirected from http://our.api.com to http://our1.api.com or some other version of the API with a different server number. The first time I authenticate with http://our1.api.com it will honor the passed in Authorization header and challenged URLCredential. But if I try to authenticate against the same API again with known bad credentials, the old URLCredential is used and I am able to get into the API when I should not be able to.
Is there a way to force URLSession to never use the cached URLCredential, or otherwise clear out the cached URLCredentials?
Creating the URLSession
let config = URLSessionConfiguration.ephemeral
config.httpAdditionalHeaders = ["Accept":"application/xml",
"Accept-Language":"en",
"Content-Type":"application/xml"]
config.requestCachePolicy = .reloadIgnoringLocalCacheData
config.urlCache = nil
self.urlSession = URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue.main)
Calling to the API
var request = URLRequest(url: thePreRedirectedUrl)
request.httpMethod = "GET"
request.addValue("Basic username:password", forHTTPHeaderField: "Authorization")
let task = urlSession?.dataTask(with: request, completionHandler: { (data, response, error) in
// pass endpoint results to completion block
completionBlock(data, response, error)
})
// run the task
if let task = task {
task.resume()
}
URLSessionDelegate and URLSessionTaskDelegate
extension ApiManager: URLSessionDelegate, URLSessionTaskDelegate {
func urlSession(_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: #escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
if challenge.previousFailureCount == 0 {
completionHandler(.useCredential, URLCredential(user: username, password: password, persistence: .none))
} else {
completionHandler(.performDefaultHandling, nil)
}
}
func urlSession(_ session: URLSession,
task: URLSessionTask,
willPerformHTTPRedirection response: HTTPURLResponse,
newRequest request: URLRequest,
completionHandler: #escaping (URLRequest?) -> Void) {
var newRequest = URLRequest(url: request.url!)
newRequest.addValue("Basic username:password", forHTTPHeaderField: "Authorization")
newRequest.httpMethod = task.originalRequest?.httpMethod
newRequest.httpBody = task.originalRequest?.httpBody
completionHandler(newRequest)
}
}
The most reliable way is to delete the credential from the user's (macOS) or app's (iOS) keychain.
See Updating and Deleting Keychain Items on Apple's developer website for details, but basically:
NSDictionary *matchingDictionary = #{
kSecClass: kSecClassInternetPassword,
kSecAttrSecurityDomain: #"example.com" // <-- This may not be quite the
// right format for the domain.
};
SecItemDelete((__bridge CFDictionaryRef) matchingDictionary);

Response Data via URLSessionUploadTask

I am writing simple handler for communication with REST API on server (currently local). Everything goes well so far with downloading and uploading data from/to server.
What I am trying to achieve now is to be able to handle JSON response returned by server after uploading data. This message is something like this:
{"message":"Record successfully added.","recordID":30}
Important is for me the recordID, because I need to assign it to relevant NSManagedObject. I use delegation attitude instead of completionHandler so I would be able to manage progress of the upload. Appropriate delegate class implements these protocols with all methods:
class ConstructoHTTPHelper:NSObject, URLSessionDelegate, URLSessionDataDelegate, URLSessionTaskDelegate, URLSessionDownloadDelegate, URLSessionStreamDelegate { ... }
Here comes the issue because as far as I create upload task with something like this:
let config = URLSessionConfiguration.default
self.session = URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue.main) //URLSession(configuration: config)
var request:URLRequest = URLRequest(url:address)
request.httpMethod = "POST"
let data = // creation of data ...
let fileURL = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent("uploadData")
do {
try data.write(to: fileURL)
} catch {
// handling error
}
self.sessionUploadTask = self.session?.uploadTask(with: request, fromFile: fileURL)
self.sessionUploadTask!.resume()
The delegate func for handling data:
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {}
returned by server is never called.
What is strange to me is that when I use completion Handler like the one below, It prints the JSON well:
self.sessionUploadTask = self.session?.uploadTask(with: request, from: data, completionHandler: { (data, response, error) in
print(NSString(data: data!, encoding: String.Encoding.utf8.rawValue)!)
})
So it looks to me that uploadTask is limited in this way. Any suggestions?
Thanks
I probably found the answer, just add this to URLSession:dataTask:didReceiveResponse:completionHandler: delegate method.
completionHandler(URLSession.ResponseDisposition.allow)
I found solution in this thread.
try this!, get a NSMutableData as buffer like this globally
fileprivate var buffer:NSMutableData = NSMutableData()
and in your URLSession delegate method add,
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let _ = error {
print(error!.localizedDescription)
}else {
// do your parsing with buffer here.
}
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
buffer.append(data)
}

HTTP Basic Auth with NSURLSession

I'm trying to implement HTTP Basic Auth with NSURLSession, but I run into several issues. Please read the entire question before responding, I doubt this is a duplicate of an other question.
According to the tests I've run, the behavior of NSURLSession is the following :
The first request is always made without the Authorization header.
If the first request fails with a 401 Unauthorized response and a WWW-Authenticate Basic realm=... header, it is automatically retried.
Before retrying the request, the session will attempt to obtain credentials by looking into the NSURLCredentialStorage of the session configuration or by calling the URLSession:task:didReceiveChallenge:completionHandler: delegate method (or both).
If credentials could be obtained the request is retried with the proper Authorization header. If not it is retried without the header (which is weird because in this case, this is exactly the same request).
If the second request succeeds, the task is transparently reported as successful and you're not even notified that the request was attempted twice. If not, the failure of the second request is reported (but not the first).
The problem I have with this behavior is that I am uploading large files to my server through multipart requests, so when the request is attempted twice, the entire POST body is sent twice which is a terrible overhead.
I have tried to manually add the Authorization header to the httpAdditionalHeaders of the session configuration, but it works only if the property is set before the session is created. Attempting to modify session.configuration.httpAdditionalHeaders afterwards doesn't work. Also the documentation clearly says that the Authorization header should not be set manually.
So my question is: If I need to start the session before I obtain the credentials and If I want to be sure that requests are always made with the proper Authorization header the first time, how do I do ?
Here is a code sample that I've used for my tests. You can reproduce all the behaviors I've described above with it.
Note that in order to be able to see the double requests you wil need to either use your own http server and log the requests or connect through a proxy that logs all requests (I've used Charles Proxy for this)
class URLSessionTest: NSObject, URLSessionDelegate
{
static let shared = URLSessionTest()
func start()
{
let requestURL = URL(string: "https://httpbin.org/basic-auth/username/password")!
let credential = URLCredential(user: "username", password: "password", persistence: .forSession)
let protectionSpace = URLProtectionSpace(host: "httpbin.org", port: 443, protocol: NSURLProtectionSpaceHTTPS, realm: "Fake Realm", authenticationMethod: NSURLAuthenticationMethodHTTPBasic)
let useHTTPHeader = false
let useCredentials = true
let useCustomCredentialsStorage = false
let useDelegateMethods = true
let sessionConfiguration = URLSessionConfiguration.default
if (useHTTPHeader) {
let authData = "\(credential.user!):\(credential.password!)".data(using: .utf8)!
let authValue = "Basic " + authData.base64EncodedString()
sessionConfiguration.httpAdditionalHeaders = ["Authorization": authValue]
}
if (useCredentials) {
if (useCustomCredentialsStorage) {
let urlCredentialStorage = URLCredentialStorage()
urlCredentialStorage.set(credential, for: protectionSpace)
sessionConfiguration.urlCredentialStorage = urlCredentialStorage
} else {
sessionConfiguration.urlCredentialStorage?.set(credential, for: protectionSpace)
}
}
let delegate = useDelegateMethods ? self : nil
let session = URLSession(configuration: sessionConfiguration, delegate: delegate, delegateQueue: nil)
self.makeBasicAuthTest(url: requestURL, session: session) {
self.makeBasicAuthTest(url: requestURL, session: session) {
DispatchQueue.main.asyncAfter(deadline: .now() + 61.0) {
self.makeBasicAuthTest(url: requestURL, session: session) {}
}
}
}
}
func makeBasicAuthTest(url: URL, session: URLSession, completion: #escaping () -> Void)
{
let task = session.dataTask(with: url) { (data, response, error) in
if let response = response {
print("response : \(response)")
}
if let data = data {
if let json = try? JSONSerialization.jsonObject(with: data, options: .allowFragments) {
print("json : \(json)")
} else if data.count > 0, let string = String(data: data, encoding: .utf8) {
print("string : \(string)")
} else {
print("data : \(data)")
}
}
if let error = error {
print("error : \(error)")
}
print()
DispatchQueue.main.async(execute: completion)
}
task.resume()
}
#objc(URLSession:didReceiveChallenge:completionHandler:)
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: #escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Swift.Void)
{
print("Session authenticationMethod: \(challenge.protectionSpace.authenticationMethod)")
if (challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPBasic) {
let credential = URLCredential(user: "username", password: "password", persistence: .forSession)
completionHandler(.useCredential, credential)
} else {
completionHandler(.performDefaultHandling, nil)
}
}
#objc(URLSession:task:didReceiveChallenge:completionHandler:)
func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: #escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Swift.Void)
{
print("Task authenticationMethod: \(challenge.protectionSpace.authenticationMethod)")
if (challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPBasic) {
let credential = URLCredential(user: "username", password: "password", persistence: .forSession)
completionHandler(.useCredential, credential)
} else {
completionHandler(.performDefaultHandling, nil)
}
}
}
Note 1: When making multiple requests in a row to the same endpoint, the behavior I've described above concerns only the first request. Subsequent requests are tried with the proper Authorization header the first time. However, if you wait some time (about 1 minute), the session will return to the default behavior (first request tried twice).
Note 2: This is not directly related, but using a custom NSURLCredentialStorage for the urlCredentialStorage of the session configuration doesn't seem to work. Only using the default value (which is the shared NSURLCredentialStorage according to the documentation) works.
Note 3: I've tried using Alamofire, but since it's based on NSURLSession, it behaves in the exact same way.
If possible, the server should respond with an error long before the client finishes sending the body. However, in many high-level server-side languages, this is difficult, and there's no guarantee that the upload will stop even if you do so.
The real problem is that you're performing a large upload using a single POST request. That make authentication problematic, and also prevents any sort of useful continuation of uploads if the connection drops midway through the upload. Chunking the upload basically solves all of your issues:
For your first request, send only the amount that will fit without adding additional Ethernet packets, i.e. compute your typical header size, mod by 1500 bytes, add a few tens of bytes for good measure, subtract from 1500, and hard-code that size for your first chunk. At most, you've wasted a few packets.
For subsequent chunks, crank the size up.
When a request fails, ask the server how much it got, and retry from where the upload left off.
Issue a request to tell the server when you've finished uploading.
Periodically purge partial uploads on the server side with a cron job or whatever.
That said, if you don't have control over the server side, the usual workaround is to sent an authenticated GET request right before your POST request. This minimizes wasted packets while still mostly working as long as the network is reliable.

Certificate for this server is invalid for self signed cert on swift 3

I am using swift 3 and hitting a web service for the first time. My web service runs over HTTPS and I want to test with encryption in place.
Here's my code so far:
let config = URLSessionConfiguration.default // Session Configuration
let session = URLSession(configuration: config) // Load configuration into Session
let url = URL(string: webService.getLoginUrl())!
let task = session.dataTask(with: url, completionHandler: {
(data, response, error) in
if error == nil {
do {
if let json =
try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? [String: Any]{
//Implement your logic
print(json)
}
} catch {
print("error in JSONSerialization")
}
} else {
print(error!.localizedDescription)
}
})
task.resume()
When I run this against my test server, which is self-signed, I get:
The certificate for this server is invalid. You might be connecting to a server that is pretending to be “10.0.0.51” which could put your confidential information at risk.
So what I'd like to do is accept all certificates when testing, but not in production.
I've found a couple of sites like:
http://www.byteblocks.com/Post/Use-self-signed-SSL-certificate-in-iOS-application
https://github.com/socketio/socket.io-client-swift/issues/326
But these appear to predate swift 3.
How do I solve this problem?
After much research, I learned about how delegates work with URLSession objects in swift 3. Too many pieces to post a link, but in the end, this was the most helpful: https://gist.github.com/stinger/420107a71a02995c312036eb7919e9f9
So, to fix the problem, I inherited my class from URLSessionDelegate and then added the following function:
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: #escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
//accept all certs when testing, perform default handling otherwise
if webService.isTesting() {
print("Accepting cert as always")
completionHandler(.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!))
}
else {
print("Using default handling")
completionHandler(.performDefaultHandling, URLCredential(trust: challenge.protectionSpace.serverTrust!))
}
}
The isTesting() call determines if I'm using the test server, and then we accept all certificates if we're in testing mode.

Resources