Swift NSURLSession HTTPS Self Sign - Request never reaches server - swift

I'm having troubles performing a HTTPS post request to my django rest api. I have django-sslserver running to expose the api on port 8000. Things seem fine, when I make a request in a browser https://server-ip-addr:8000/api_view/ my browser complains "Hey! This guys certificate is self signed!" I say "yeah I know, that's me" and continue into the dangerous response.
Anyways I'm attempting to perform the same in Swift for an iOS application. I've found from this link here on implementing NSURLSession delegate protocols/functions NSURLSessionDelegate.URLSession() and NSURLSessionTaskDelegate.URLSession(). I've tweaked the example so that it atomically performs login attempts to my sslserver.
I've done so in the following code, a class implementing the two protocols above designed to pass username/password credentials to the server and wait for a response.
class SecureLogin: NSObject, NSURLSessionDelegate, NSURLSessionTaskDelegate {
func attemptLogin(username: String, password: String,
callback: ((NSData!,NSURLResponse!,NSError!) -> Void)?) {
println("inside attempt login")
var request = NSMutableURLRequest(URL: NSURL(string: "https://147.222.164.91:8000/ldapauth/")!)
request.HTTPMethod = "POST"
var params = ["username":username, "password":password] as Dictionary<String, String>
var err: NSError?
request.HTTPBody = NSJSONSerialization.dataWithJSONObject(params, options: nil, error: &err)
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("application/json", forHTTPHeaderField: "Accept")
var configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
var session = NSURLSession(configuration: configuration,
delegate: self,
delegateQueue:NSOperationQueue.mainQueue())
var task = session.dataTaskWithRequest(request,callback)
task.resume()
}
func URLSession(session: NSURLSession,
didReceiveChallenge challenge: NSURLAuthenticationChallenge,
completionHandler: (NSURLSessionAuthChallengeDisposition,NSURLCredential!) -> Void) {
println("Challenge received")
completionHandler(NSURLSessionAuthChallengeDisposition.UseCredential, NSURLCredential(forTrust: challenge.protectionSpace.serverTrust))
}
func URLSession(session: NSURLSession,
task: NSURLSessionTask,
willPerformHTTPRedirection response: NSHTTPURLResponse,
newRequest request: NSURLRequest,
completionHandler: (NSURLRequest!) -> Void) {
println("Redirection received")
var newRequest : NSURLRequest? = request
println(newRequest?.description)
completionHandler(newRequest)
}
}
So I go to attempt to perform the attemptLogin() function, providing the simple callback function to confirm a response
var gatekeeper = SecureLogin()
gatekeeper.attemptLogin(username, password: password, callback: {data, response, error -> Void in
println("inside gatekeeper")
}
println("beginning wait")
sleep(25)
I sleep the thread 25 seconds to keep the process alive long enough for the response to come in.
The output on the console looks like:
inside attempt login
beginning wait
Then the program dies, no response/"inside gatekeeper" message received, my django server terminal doesn't show any received requests either. I ran a sanity check: I've commented out the implementations of the delegate methods and the server receives the request, responds with:
inside attempt login
beginning wait
2015-01-27 11:29:37.192 LdapAuthSecure[12783:1475994] NSURLConnection/CFURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9812)
the expected error when there is no protocol for handling an authorization challenge.
Can anyone spot any problems with my implementation of the NSURLSession or its delegates? I find it very strange that the request doesn't even reach the server. Thank you for any help!

If this is iOS 9, and if you built your app against the iOS 9 (or later) SDK, you'll also have to tweak your Info.plist file to tell it to allow insecure loads. Otherwise, the URL loading system won't get as far as calling your authentication handler.

so I've never coded for iOS but I do know from memory that native apps will not popup a dialog nor accept self signed certificates by default.
If you own a domain (or if not, it's probably easier) you can get a free certificate from https://www.startssl.com/ or https://buy.wosign.com/free/
Or you can install the self signed certificate by emailing it to your phone and then opening it.
You can remove the self signed certificate from the profiles page in Settings.
I also found this answer for you: https://stackoverflow.com/a/22674004/4837003
But that looks like it will disable validation.

Related

Correct Alamofire retry for JWT if status 401?

I am trying to make a retry for my Alamofire Interceptor because I work with JSON Web Token. Adapt works great. But the server updates the Access token every 10 minutes after user registration or authorization. After 10 mins Access token doesn't work anymore, and the server response is 401. So I need to Refresh the token when the status is 401. As I mentioned above, adapt works great. But I need help understanding how to deal with retry. Below is my Interceptor:
class RequestInterceptor: Alamofire.RequestInterceptor {
func adapt( _ urlRequest: URLRequest, for session: Session, completion: #escaping (Result<URLRequest, Error>) -> Void) {
var urlRequest = urlRequest
urlRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
completion(.success(urlRequest))
}
func retry( _ request: Request, for session: Session, dueTo error: Error, completion: #escaping (RetryResult) -> Void) {
guard let response = request.task?.response as? HTTPURLResponse, response.statusCode == 401 else {
completion(.doNotRetryWithError(error))
return
}
}
}
My View Model:
func refreshTokenFunc() {
AF.request(TabBarModel.Request.refreshTokenUrl, method: .post, parameters: parameters, encoder: JSONParameterEncoder.default, interceptor: RequestInterceptor()).response { response in
...
And usage (I work with SwiftUI):
.task {
tabBarViewModel.refreshTokenFunc()
}
I was trying with some examples from the Internet. But it doesn't work for me.
In you retry you need to call the completion handler on both sides of the guard, not just in the else side. completion(.retry) is common but you could also track a delay to make sure you don't overload the backend.
Additionally, you should be validating response and checking the error, not reaching directly into request.task.
AF.request(...).validate()... // Ensure the response code is within range.
// In retry
guard let error = error.asAFError, error.responseCode == 401 else { ... }

Swift URLSession not working for localhost calls

I'm writing a basic API call in Swift using URLRequests, and for whatever reason my call is never executed. I have multiple calls to an external server API using the same method and the functionality is just as expected, however, for my server running locally I get no response or even behavior within the dataTask closure.
I have tried any relevant solutions I could find online such as: Swift URL Session and URL Request not working and Swift 3, URLSession dataTask completionHandler not called. But none of these solutions seem to fix my issue. I know that the local API is working as any calls through Postman go through without fail, yet even after using the Swift snippet provided by Postman, I get no functionality.
func doFoo(id: String, completion: #escaping ([[Float]]) -> ()) {
let semaphore = DispatchSemaphore(value: 0)
var request = URLRequest(url: URL(string: "127.0.0.1:8080/doFoo/\(id)")!, timeoutInterval: Double.infinity)
request.httpMethod = "GET"
print("THIS IS REACHED")
let task = URLSession.shared.dataTask(with: request) { data, response, error in
print("THIS IS NEVER REACHED")
guard let data = data else {
self.semaphore.signal()
return
}
do {
// Decode json using JSONDecoder
// Call completion with JSON data
} catch {
print(error)
}
self.semaphore.signal()
}
task.resume()
self.semaphore.wait()
}
Other posts suggest that this could be an issue with the thread or execution completing before the closure is executed; while I am not super familiar with how the request executes and the behavior of semaphores, my understanding is that they are a way to request threads and prevent the above from happening.
If anyone more familiar with these topics could help me identify and understand why this issue is occurring, it would be greatly appreciated!

URLSession Response doesn't contain headers from last redirect

I have an URL that I, when called in a webbrowser, will redirect me 2 times and in the response header of the second redirect it will send the Information that I want to extract.
So to automatically extract that information in swift, I wrote this short piece of code that makes the HTTP Request and then prints the response headers:
printv(text: "Loading JSID Location")
req = URLRequest.init(url: JSIDLocation!)
var task : URLSessionDataTask
task = URLSession.shared.dataTask(with: req) {(data, response, error) in
if let res = response as? HTTPURLResponse {
res.allHeaderFields.forEach { (arg0) in
let (key, value) = arg0
self.printv(text: "\(key): \(value)")
}
}
self.printv(text: String.init(data: data!, encoding: String.Encoding.utf8)!)
}
task.resume()
(printv is a function that will format the string and print it to a label)
So when I run this, I expect it to print the response headers and the body of the last redirect, but what actually happens is that i just prints response headers and body of the original URL. As those don't contain the information im looking for, that won't help me. I already googled my problem, and I found out that HTTP Redirects by default are activated in URLSessions and that you'd had to mess with URLSessionDelegates in order to deactivate them but that's definetly not something I did.
Thank you for your help!
If you want redirect information, you need to become the URLSessionDataTaskDelegate.
let session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
Then you need to implement, the redirection delegate function and be sure to call the completion handler with the given new redirect request:
func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: #escaping (URLRequest?) -> Void) {
// operate on response to learn about the headers here
completionHandler(request)
}

Trying to understand URLSession Authentication challenges

I am attempting to download a PDF from a URL.
private func downloadSessionWithFileURL(_ url: URL){
var request = URLRequest(url: url)
request.addValue("gzip, deflate", forHTTPHeaderField: "Accept-Encoding")
let sessionConfig = URLSessionConfiguration.default
let session = URLSession(configuration: sessionConfig, delegate: self, delegateQueue: nil)
session.downloadTask(with: request).resume()
}
This calls its delegate method
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: #escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
if challenge.previousFailureCount > 0 {
completionHandler(Foundation.URLSession.AuthChallengeDisposition.cancelAuthenticationChallenge, nil)
}
if let serverTrust = challenge.protectionSpace.serverTrust {
completionHandler(Foundation.URLSession.AuthChallengeDisposition.useCredential, URLCredential(trust: serverTrust))
} else {
print("unknown state. error: \(String(describing: challenge.error))")
}
}
The URLAuthenticationChallenges protectionSpace is always serverTrust.
When the URL of the PDF is attempted to be accessed it redirects user to a login screen. I would have thought there would be another call to
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: #escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
requiring user to enter their credentials but there isn't. So the download task attempts to download the contents of the redirected URL which is a login screen.
My Questions are.
What triggers a URLAuthenticationChallenge for a username and password. is it a specific header value in the HTML?
Which URLAuthenticationChallenge protectionSpace should I be expecting for a username password request from a server.
There are two different delegate protocols: for the URLSession itself, and its tasks.
URLSessionDelegate has: public func urlSession(_:didReceive:completionHandler:)
URLSessionTaskDelegate has: public func urlSession(_:task:didReceive:completionHandler:)
The URLSessionDelegate is used for server trust issues (e.g. allowing SSL trust when running through Charles or other proxy). The URLSessionTaskDelegate is used for authentication of an individual task.
So to get your authentication challenge, add this to your class:
extension MyClass: URLSessionTaskDelegate {
public func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: #escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodDefault ||
challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPBasic {
let credential = URLCredential(user: self.basicAuthUserName,
password: self.basicAuthPassword,
persistence: .forSession)
completionHandler(.useCredential, credential)
}
else {
completionHandler(.performDefaultHandling, nil)
}
}
}
Some basics of SSL:
How SSL works? When client establishes the connection with server (called SSL handshake):
Client connects to server and requests server identify itself.
Server sends certificate to client (include public key)
Client checks if that certificate is valid. If it is, client creates a symmetric key (session key), encrypts with public key, then sends back to server
Server receives encrypted symmetric key, decrypts by its private key, then sends acknowledge packet to client
Client receives ACK and starts the session
1.What triggers a URLAuthenticationChallenge for a username and password. is it a specific header value in the HTML?
If you an have https connection, these methods will be triggered. These are for security purpose to prevent the man in the middle attack. For e.g, I can set up charles proxy server, install the public certificate on simulator/device and can monitor all the request that the app is sending to the actual server and thus obtain the sensitive information(API Keys, token, request Headers, request body etc) which I need to hide from attackers.
Which URLAuthenticationChallenge protectionSpace should I be expecting
for a username password request from a server.
You can either Compare the server certificate with the local certificates that you have in your apps:
if let serverCertificate = SecTrustGetCertificateAtIndex(trust, 0) {
let serverCertificateData = SecCertificateCopyData(serverCertificate) as Data
let localCer = Bundle.main.path(forResource: "fileName", ofType: "cer")
if let localCer = localCer {
if localCer.isEqual(to: serverCertificate) { completionHandler(URLSession.AuthChallengeDisposition.useCredential, URLCredential(trust:serverTrust))
return
}
}
}
or you can compare the public keys:
if let serverCertificate = SecTrustGetCertificateAtIndex(trust, 0), let serverCertificateKey = publicKey(for: serverCertificate) {
if pinnedKeys().contains(serverCertificateKey) {
completionHandler(.useCredential, URLCredential(trust: trust))
return
}
}
Comparing public keys is a better approach as when comparing certificates, you have to keep a copy of the local certificate in the app and when the certificates expires which will have to update the certificates in the app, which require an update in the app store.

Making Repeated Requests to API

I know how to make a regular API call using swift. What I am not able to understand is how to make the API call to be repeated until required.
I want to call the API every one second
API Call Code Snippet:
let url = URL(string: "https://api.darksky.net/forecast/34eaef38915078ea03c22bb9063bd7ea/37.8267,-122.4233")
let request = URLRequest(url: url!, cachePolicy: URLRequest.CachePolicy.reloadIgnoringCacheData, timeoutInterval: 10)
let session = URLSession(configuration: URLSessionConfiguration.default, delegate: nil, delegateQueue: OperationQueue.main)
let task: URLSessionDataTask = session.dataTask(with: request, completionHandler: { (data: Data?, response: URLResponse?, error: Error?) in
if let error = error {
print(error)
} else if let data = data,
let dataDictionary = try! JSONSerialization.jsonObject(with: data, options: []) as? NSDictionary {
print("API Data:")
print(dataDictionary)
}
})
task.resume()
Note: This is not the actual API I will be calling
Ideally, for software solutions like financials you mentioned, the server must have support for some sort of Long Polling / websockets mechanism where once connection is established server feeds the client with new values whenever there are updates (refer : https://stackoverflow.com/a/12855533/1436617)
If server does not support : (Not the ideal solution) :
You can actually use recursion in this. On response (both success & failure) of the request again call the same function. That way you can continuously keep polling.
Remember to keep request timer short (5 or 10 seconds instead of 60 seconds) so that if there happens to be an network issue you can quickly make the next call.