[GH-ISSUE #493] 401 error for multipart uploads, other requests work #327

Open
opened 2026-03-03 16:47:43 +03:00 by kerem · 3 comments
Owner

Originally created by @hutattedonmyarm on GitHub (Sep 29, 2018).
Original GitHub issue: https://github.com/OAuthSwift/OAuthSwift/issues/493

Description:

I'm getting a 401 response trying to upload a file. Other requests (GET, "regular" POST, etc work perfectly fine.
So this request:

let imgData = Data(...)

auth.client.postImage(url, parameters: params, image: imgData, success: { (response) in
    print(response)
}, failure: { (error) in
    print(error)
})

throws a 401, while

let imgData = Data(...)

auth.client.post(url, parameters: params, headers: nil, body: data, success: { (response) in
    print(response)
}, failure: { (error) in
    print(error)
})

completes successfully.
Additionally an error is logged during the failing request:

CredStore - performQuery - Error copying matching creds.  Error=-25300, query={
    class = inet;
    "m_Limit" = "m_LimitAll";
    ptcl = htps;
    "r_Attributes" = 1;
    sdmn = "Web Password";
    srvr = "master.apis.dev.openstreetmap.org";
    sync = syna;
}

OAuth Provider? (Twitter, Github, ..):

OpenStreetMap

OAuth Version:

  • Version 1
  • Version 2

OS (Please fill the version) :

  • iOS :
  • OSX :
  • TVOS :
  • WatchOS :

Installation method:

  • Carthage
  • CocoaPods
  • Manually

Library version:

  • head
  • v1.2.1
  • v1.2 (Swift 4.0)
  • v1.0.0
  • v0.6
  • other: (Please fill in the version you are using.)

Xcode version:

  • 9.3 (Swift 4.1)

  • 9.0 (Swift 4.0)

  • 9.0 (Swift 3.2)

  • 8.x (Swift 3.x)

  • 8.0 (Swift 2.3)

  • 7.3.1

  • other: 10.0 (Swift 4.2)

  • objective c

Originally created by @hutattedonmyarm on GitHub (Sep 29, 2018). Original GitHub issue: https://github.com/OAuthSwift/OAuthSwift/issues/493 ### Description: I'm getting a 401 response trying to upload a file. Other requests (GET, "regular" POST, etc work perfectly fine. So this request: let imgData = Data(...) auth.client.postImage(url, parameters: params, image: imgData, success: { (response) in print(response) }, failure: { (error) in print(error) }) throws a 401, while let imgData = Data(...) auth.client.post(url, parameters: params, headers: nil, body: data, success: { (response) in print(response) }, failure: { (error) in print(error) }) completes successfully. Additionally an error is logged during the failing request: CredStore - performQuery - Error copying matching creds. Error=-25300, query={ class = inet; "m_Limit" = "m_LimitAll"; ptcl = htps; "r_Attributes" = 1; sdmn = "Web Password"; srvr = "master.apis.dev.openstreetmap.org"; sync = syna; } ### OAuth Provider? (Twitter, Github, ..): OpenStreetMap ### OAuth Version: - [x] Version 1 - [ ] Version 2 ### OS (Please fill the version) : - [x] iOS : - [ ] OSX : - [ ] TVOS : - [ ] WatchOS : ### Installation method: - [ ] Carthage - [ ] CocoaPods - [x] Manually ### Library version: - [x] head - [ ] v1.2.1 - [ ] v1.2 (Swift 4.0) - [ ] v1.0.0 - [ ] v0.6 - [ ] other: (Please fill in the version you are using.) ### Xcode version: - [ ] 9.3 (Swift 4.1) - [ ] 9.0 (Swift 4.0) - [ ] 9.0 (Swift 3.2) - [ ] 8.x (Swift 3.x) - [ ] 8.0 (Swift 2.3) - [ ] 7.3.1 - [ ] other: 10.0 (Swift 4.2) - [ ] objective c
Author
Owner

@phimage commented on GitHub (Sep 29, 2018):

I see no difference between the two codes posted
with my tiny mobile phone screen
Where is the difference?

<!-- gh-comment-id:425638486 --> @phimage commented on GitHub (Sep 29, 2018): I see no difference between the two codes posted with my tiny mobile phone screen Where is the difference?
Author
Owner

@hutattedonmyarm commented on GitHub (Sep 29, 2018):

I see no difference between the two codes posted
with my tiny mobile phone screen
Where is the difference?

I copied the wrong code block. Now there should be a difference

<!-- gh-comment-id:425638593 --> @hutattedonmyarm commented on GitHub (Sep 29, 2018): > I see no difference between the two codes posted > with my tiny mobile phone screen > Where is the difference? I copied the wrong code block. _Now_ there should be a difference
Author
Owner

@hutattedonmyarm commented on GitHub (Oct 1, 2018):

I wrote an extension to make it work. Some of it is simply duplicating already existing functionality and could probably be replaced by the existing ones, but I haven't gotten around to refining it yet.

/*
 * Most of this is taken from: https://stackoverflow.com/a/26163136/893418
 * and from: https://hannah.wf/twitter-oauth-simple-curl-requests-for-your-own-data/
 */

extension OAuthSwiftClient {
    /*
     * Creates a POST request to upload a file as multipart/form-data using OAuth 1.0 for
     * authorization. Takes additional parameters.
     */
    func uploadFile(url:String, filePath:String, fileParameter: String, mimeType: String?, params: OAuthSwift.Parameters,  success:@escaping OAuthSwiftHTTPRequest.SuccessHandler, failure:@escaping OAuthSwiftHTTPRequest.FailureHandler) {
        var oauth = [
            "oauth_consumer_key" : credential.consumerKey,
            "oauth_nonce" : String(Int64(Date().timeIntervalSince1970)),
            "oauth_signature_method":"HMAC-SHA1",
            "oauth_token":credential.oauthToken,
            "oauth_timestamp":String(Int64(Date().timeIntervalSince1970)),
            "oauth_version":"1.0"
        ]
        let method = "POST"
        /*
         * Collect and prepare the neccassary data for authorization
         * The signature needs to be signed with HMAC-SHA1 for OSM
         * OAuth 1.0 uses a composite key, made up of the consumer secret and oauth token secret
         */
        let baseInfo = buildBaseString(baseURI: url, method: method, params: oauth)
        let compositeKey = "\(urlEncode(credential.consumerSecret))&\(credential.oauthTokenSecret)"
        let oauthSignature = HMAC.sign(hashMethod: .sha1, key:  compositeKey.data(using: .utf8)!, message: baseInfo.data(using: .utf8)!)!.base64EncodedString()
        oauth["oauth_signature"] = oauthSignature
        
        let boundary = "AS-boundary-\(arc4random())-\(arc4random())"
        
        var r = URLRequest(url: URL(string:url)!)
        r.addValue(buildAuthorizationHeader(oauth: oauth), forHTTPHeaderField: "Authorization")
        r.addValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
        r.httpMethod = method
        do {
            // Add the file to the request
            r.httpBody = try createBody(with: params, filePathKey: fileParameter, path: filePath, boundary: boundary, mimetype: mimeType)
        } catch {
            failure(OAuthSwiftError.requestError(error: error, request: r))
            return
        }
        
        let session = URLSession(configuration: .default)
        let mTask = session.dataTask(with: r){ (data, response, error) in
            if let e = error {
                failure(OAuthSwiftError.requestError(error: e, request: r))
            }
            if let data = data, let response = response as? HTTPURLResponse {
                let resp = OAuthSwiftResponse(data: data, response: response, request: r)
                success(resp)
            }
        }
        mTask.resume()
    }
    
    /*
     * The base info, which will be the message of the oauth signature.
     * It consists of the parameters (without the file), the HTTP Method, and the url of the request
     */
    private func buildBaseString(baseURI:String, method:String, params:OAuthSwift.Parameters) -> String {
        var r = [String]()
        params.keys.sorted().forEach { r.append("\($0)=\(urlEncode(String(describing: params[$0]!)))")}
        return "\(method)&\(urlEncode(baseURI))&\(urlEncode(r.joined(separator: "&")))"
    }
    
    /*
     * Percent-Escapes strings for OAuth
     * Can't use the extension provided by OAuthSwift, due to its protection level
     */
    private func urlEncode(_ s:String) -> String {
        let customAllowedSet = CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~")
        return s.addingPercentEncoding(withAllowedCharacters: customAllowedSet)!
    }
    
    /*
     * Turns the OAuth header params into a header
     */
    private func buildAuthorizationHeader(oauth:[String:String]) -> String {
        var v = [String]()
        oauth.forEach{ v.append("\($0.key)=\"\(urlEncode($0.value))\"") }
        return "OAuth \(v.joined(separator: ","))"
    }
    /*
     * Creates the body of the POST request from the file
     */
    private func createBody(with parameters: OAuthSwift.Parameters = [:], filePathKey: String, path: String, boundary: String, mimetype:String?) throws -> Data {
        var body = Data()
        
        for (key, value) in parameters {
            body.append("--\(boundary)\r\n")
            body.append("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n")
            body.append("\(value)\r\n")
        }
        let url = URL(fileURLWithPath: path)
        let filename = url.lastPathComponent
        let data = try Data(contentsOf: url)
        
        body.append("--\(boundary)\r\n")
        body.append("Content-Disposition: form-data; name=\"\(filePathKey)\"; filename=\"\(filename)\"\r\n")
        body.append("Content-Type: \(mimetype ?? mimeType(for: path))\r\n\r\n")
        body.append(data)
        body.append("\r\n")
        
        body.append("--\(boundary)--\r\n")
        return body
    }
    
    /*
     * Determines the mime type of a file
     * based on its extension
     */
    private func mimeType(for path: String) -> String {
        let url = URL(fileURLWithPath: path)
        let pathExtension = url.pathExtension
        
        if let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension as NSString, nil)?.takeRetainedValue() {
            if let mimetype = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType)?.takeRetainedValue() {
                return mimetype as String
            }
        }
        return "application/octet-stream"
    }
}

extension Data {
    
    /// Append string to Data
    ///
    /// Rather than littering my code with calls to `data(using: .utf8)` to convert `String` values to `Data`, this wraps it in a nice convenient little extension to Data. This defaults to converting using UTF-8.
    ///
    /// - parameter string:       The string to be added to the `Data`.
    
    mutating func append(_ string: String, using encoding: String.Encoding = .utf8) {
        if let data = string.data(using: encoding) {
            append(data)
        }
    }
}
<!-- gh-comment-id:426018519 --> @hutattedonmyarm commented on GitHub (Oct 1, 2018): I wrote an extension to make it work. Some of it is simply duplicating already existing functionality and could probably be replaced by the existing ones, but I haven't gotten around to refining it yet. ``` /* * Most of this is taken from: https://stackoverflow.com/a/26163136/893418 * and from: https://hannah.wf/twitter-oauth-simple-curl-requests-for-your-own-data/ */ extension OAuthSwiftClient { /* * Creates a POST request to upload a file as multipart/form-data using OAuth 1.0 for * authorization. Takes additional parameters. */ func uploadFile(url:String, filePath:String, fileParameter: String, mimeType: String?, params: OAuthSwift.Parameters, success:@escaping OAuthSwiftHTTPRequest.SuccessHandler, failure:@escaping OAuthSwiftHTTPRequest.FailureHandler) { var oauth = [ "oauth_consumer_key" : credential.consumerKey, "oauth_nonce" : String(Int64(Date().timeIntervalSince1970)), "oauth_signature_method":"HMAC-SHA1", "oauth_token":credential.oauthToken, "oauth_timestamp":String(Int64(Date().timeIntervalSince1970)), "oauth_version":"1.0" ] let method = "POST" /* * Collect and prepare the neccassary data for authorization * The signature needs to be signed with HMAC-SHA1 for OSM * OAuth 1.0 uses a composite key, made up of the consumer secret and oauth token secret */ let baseInfo = buildBaseString(baseURI: url, method: method, params: oauth) let compositeKey = "\(urlEncode(credential.consumerSecret))&\(credential.oauthTokenSecret)" let oauthSignature = HMAC.sign(hashMethod: .sha1, key: compositeKey.data(using: .utf8)!, message: baseInfo.data(using: .utf8)!)!.base64EncodedString() oauth["oauth_signature"] = oauthSignature let boundary = "AS-boundary-\(arc4random())-\(arc4random())" var r = URLRequest(url: URL(string:url)!) r.addValue(buildAuthorizationHeader(oauth: oauth), forHTTPHeaderField: "Authorization") r.addValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") r.httpMethod = method do { // Add the file to the request r.httpBody = try createBody(with: params, filePathKey: fileParameter, path: filePath, boundary: boundary, mimetype: mimeType) } catch { failure(OAuthSwiftError.requestError(error: error, request: r)) return } let session = URLSession(configuration: .default) let mTask = session.dataTask(with: r){ (data, response, error) in if let e = error { failure(OAuthSwiftError.requestError(error: e, request: r)) } if let data = data, let response = response as? HTTPURLResponse { let resp = OAuthSwiftResponse(data: data, response: response, request: r) success(resp) } } mTask.resume() } /* * The base info, which will be the message of the oauth signature. * It consists of the parameters (without the file), the HTTP Method, and the url of the request */ private func buildBaseString(baseURI:String, method:String, params:OAuthSwift.Parameters) -> String { var r = [String]() params.keys.sorted().forEach { r.append("\($0)=\(urlEncode(String(describing: params[$0]!)))")} return "\(method)&\(urlEncode(baseURI))&\(urlEncode(r.joined(separator: "&")))" } /* * Percent-Escapes strings for OAuth * Can't use the extension provided by OAuthSwift, due to its protection level */ private func urlEncode(_ s:String) -> String { let customAllowedSet = CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~") return s.addingPercentEncoding(withAllowedCharacters: customAllowedSet)! } /* * Turns the OAuth header params into a header */ private func buildAuthorizationHeader(oauth:[String:String]) -> String { var v = [String]() oauth.forEach{ v.append("\($0.key)=\"\(urlEncode($0.value))\"") } return "OAuth \(v.joined(separator: ","))" } /* * Creates the body of the POST request from the file */ private func createBody(with parameters: OAuthSwift.Parameters = [:], filePathKey: String, path: String, boundary: String, mimetype:String?) throws -> Data { var body = Data() for (key, value) in parameters { body.append("--\(boundary)\r\n") body.append("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n") body.append("\(value)\r\n") } let url = URL(fileURLWithPath: path) let filename = url.lastPathComponent let data = try Data(contentsOf: url) body.append("--\(boundary)\r\n") body.append("Content-Disposition: form-data; name=\"\(filePathKey)\"; filename=\"\(filename)\"\r\n") body.append("Content-Type: \(mimetype ?? mimeType(for: path))\r\n\r\n") body.append(data) body.append("\r\n") body.append("--\(boundary)--\r\n") return body } /* * Determines the mime type of a file * based on its extension */ private func mimeType(for path: String) -> String { let url = URL(fileURLWithPath: path) let pathExtension = url.pathExtension if let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension as NSString, nil)?.takeRetainedValue() { if let mimetype = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType)?.takeRetainedValue() { return mimetype as String } } return "application/octet-stream" } } extension Data { /// Append string to Data /// /// Rather than littering my code with calls to `data(using: .utf8)` to convert `String` values to `Data`, this wraps it in a nice convenient little extension to Data. This defaults to converting using UTF-8. /// /// - parameter string: The string to be added to the `Data`. mutating func append(_ string: String, using encoding: String.Encoding = .utf8) { if let data = string.data(using: encoding) { append(data) } } } ```
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/OAuthSwift#327
No description provided.