Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift Alamofire file upload with signed request: how to send authorization headers?

Scenario:

  • iPhone iOS 8+ app
  • Logged in user will upload a profile picture

The app already uses Alamofire to make signed requests to the backend API. Really simple: the app sends three specific HTTP headers (Authorization, X-Api-Key and timestamp) for the request to be signed. Calling Alamofire.request it's easy to send headers as parameter so it's working beautifully.

Now the users need to be able to upload their profile picture. Since the user is already logged into the app, the backend API will know which user is sending the picture by it's signed request - and that's the tricky part I've been struggling with for the past few hours. Alamofire.upload accepts completely different parameters from .request, so I can't figure out how to send headers when uploading a file.

Tried the old Alamofire.Manager.session.configuration.HTTPAdditionalHeaders, but it's no longer supported. Found tons code examples of file upload, none consider sending custom headers.

How can I send custom headers when using Alamofire.upload method?

typealias requestDataType = [String:AnyObject]
private func signRequest(data: requestDataType) -> [String:String] {
    var headers = [String:String]()

    var authString = ""
    var signatureHeaders = ""

    // Iterates over SORTED data dictionary to build headers
    for (k,v) in (data.sort{$0.0 < $1.0}) {
        if !authString.isEmpty {
            authString += "\n"
            signatureHeaders += " "
        }
        authString += "\(k): \(v)"
        signatureHeaders += "\(k)"
        headers[k] = "\(v)"
    }

    let userApiKey = _loggedInUser!["api_key"].string!
    let signature = authString.sha256(_loggedInUser!["api_secret"].string!)

    headers["X-Api-Key"] = userApiKey
    headers["Authorization"] = "Signature headers=\"\(signatureHeaders)\",keyId=\"\(userApiKey)\",algorithm=\"hmac-sha256\",signature=\"\(signature)\""

    return headers
}

func uploadProfilePicture(photo: UIImage, callback: apiCallback){
    guard let userId = _loggedInUser?["pk"].int else {
        callback(Response(success: false, responseMessage: "User not logged in"))
        return
    }

    let requestData: requestDataType = ["timestamp": "\(Int(NSDate().timeIntervalSince1970))"]

    let aManager = Manager.sharedInstance
    print(self.signRequest(requestData)) // Prints correct headers (Authorization, X-Api-Key, timestamp)
    aManager.session.configuration.HTTPAdditionalHeaders = self.signRequest(requestData)
    print(aManager.session.configuration.HTTPAdditionalHeaders) // Prints default headers, completely ignoring my custom headers

    aManager.upload(.POST, "\(_apiBaseUrl)profiles/\(userId)/photo/", multipartFormData: { multipartFormData in
        if let imageData = UIImageJPEGRepresentation(photo, 0.8) {
            multipartFormData.appendBodyPart(data: imageData, name: "upload", fileName: "userphoto.jpg", mimeType: "image/jpeg")
        }

        for (key, value) in requestData {
            multipartFormData.appendBodyPart(data: value.dataUsingEncoding(NSUTF8StringEncoding)!, name: key)
        }

        }, encodingCompletion: {
            encodingResult in

            debugPrint(encodingResult)
    })
}

The requests goes through. In the backend log I can see the request returned HTTP 403 - Not authorized as it wasn't possible to sign the request. Printing the request headers, no custom auth headers were received by the server.

like image 539
mathielo Avatar asked Feb 22 '16 03:02

mathielo


1 Answers

Before init, I want to share a free tool (chrome app) very useful during this type of works: DHC Rest Client : with this tool you can verify if your parameters, headers and your upload files work with the type of request you want to make to the server.

So, this work with Swift 2.x and Alamofire 3.x:

First of all prepare your headers:

let headers = [
                "Content-Type": "application/zip",
                "X-Api-Key": userApiKey,
                ...whatever you need on headers..
            ]

So, supposing you must send a zip file and the response will be a TEXT/HTML response type (a simple string with SUCCESS or ERROR):

let filePath: String! = "/Users/admin.../Documents/myZipFile.zip"
var zipData: NSData! = NSData()
do {
    zipData = try NSData(contentsOfFile: filePath, options: NSDataReadingOptions.DataReadingMappedIfSafe)
} catch {
    print("- error during get nsdata from zip file\(error)")
}
let url :String! = String(format:"...myUrl?key1=%@&key2=%@",value1,value2)
Alamofire.upload(.POST, url, headers: headers, data: zipData)
                .responseString { response in
            if response.result.isSuccess {
                  let responseValue = response.result.value
                  print("Response value is: \(responseValue)")
            } else {
               var statusCode = 0
               if (response.response != nil) {
                  statusCode = (response.response?.statusCode)!
               }
               print("Error: \(response.result.error!) with statusCode: \(statusCode)")
            }

Thats all but if you want to use multipartformdata you can do it passing your headers through headers dictionary with:

.upload(<#T##method: Method##Method#>, <#T##URLString: URLStringConvertible##URLStringConvertible#>, headers: <#T##[String : String]?#>, multipartFormData: <#T##MultipartFormData -> Void#>

like image 163
Alessandro Ornano Avatar answered Nov 02 '22 20:11

Alessandro Ornano