Search code examples
iosamazon-web-servicesswift3aws-sdkamazon-cognito

How to access AWS API gateway by authorization in swift 3?


When I access AWS gateway by calculating AWS signature by Alamofire. It responds error message "The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details."

Is there any way to access to AWS API gateway in swift 3. Could you please provide me some simple swift code if possible. Thanks in advance!

private var configuration:AWSServiceConfiguration?
private var awsCredential = AWSCredential()

private func apiGatewaySimple(){
    let date = URLRequestSigner().iso8601()
    let xAmzStamp = date.short
    guard let URL = URL(string: "xxxxx") else { return }

    var request = URLRequest(url: URL)
    let url = request.url
    let host = url?.host
    let credentialProvider = AWSCognitoCredentialsProvider(regionType: .APNortheast1, identityPoolId: Constants.IDENTITY_POOL_ID)
    let endpoint = AWSEndpoint.init(region: .APNortheast1, service: .APIGateway, url: url)
    self.configuration = AWSServiceConfiguration.init(region: .APNortheast1, endpoint: endpoint, credentialsProvider: credentialProvider)
    let signer : AWSSignatureV4Signer = AWSSignatureV4Signer(credentialsProvider: self.configuration?.credentialsProvider, endpoint: endpoint)

    self.configuration?.requestInterceptors = [AWSNetworkingRequestInterceptor(),signer]
    _ = self.configuration?.responseInterceptors
    _ = self.configuration?.endpoint
    request.addValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")

    let requestDate = Date()
    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat = "yyyyMMdd'T'HHmmss'Z'"
    _ = dateFormatter.string(from: requestDate)


    let params:[String :Any]=["xxx" : "xxx" as AnyObject,
                              "xxxx" : "xxx" as AnyObject ]

    let jsonData = try? JSONSerialization.data(withJSONObject: params, options: [])
    let json = jsonData

    request.httpMethod = HttpMethod.post.rawValue
    request.httpBody = jsonData as! Data
    request.setValue(date.full, forHTTPHeaderField: "X-Amz-Date")
    request.setValue(self.awsCredential.sessionKey, forHTTPHeaderField: "X-Amz-Security-Token")
    request.setValue(json?.count.description, forHTTPHeaderField: "Content-Length")
    request.setValue(host, forHTTPHeaderField: "Host")

    let contentLength = json?.count
    let cfpath = request.url
    let query = cfpath?.query
    let hash = AWSSignatureSignerUtility.hash(request.httpBody)
    let contentsha256 = AWSSignatureSignerUtility.hexEncode(NSString.init(data: hash!, encoding: String.Encoding.ascii.rawValue)! as String)
    let canonicalRequest = AWSSignatureV4Signer.getCanonicalizedRequest(request.httpMethod, path: "xxxxx", query: query, headers: request.allHTTPHeaderFields, contentSha256: contentsha256)
    let scope = String(format: "%@/%@/%@/%@", xAmzStamp, "ap-northeast-1","execute-api",AWSSignatureV4Terminator)
    let signingCredential = String(format: "%@/%@", self.awsCredential.accessKey!,scope)

    let awsSignatureSignerV4 = AWSSignatureV4Signer(credentialsProvider: configuration?.credentialsProvider,endpoint:endpoint)


    _ = awsSignatureSignerV4?.interceptRequest(request as! NSMutableURLRequest)


    let canonicalRequestHash = AWSSignatureSignerUtility.hashString(canonicalRequest)

    let stringToSign = String(format: "%@/%@/%@/%@",AWSSignatureV4Algorithm,request.value(forHTTPHeaderField: "X-Amz-Date")!,scope,AWSSignatureSignerUtility.hexEncode(canonicalRequestHash))

    let kSigning = AWSSignatureV4Signer.getV4DerivedKey(self.awsCredential.secretKey, date: xAmzStamp, region: "ap-northeast-1", service: "execute-api")
    let signature = AWSSignatureSignerUtility.sha256HMac(with: stringToSign.data(using: .utf8), withKey: kSigning)

    let credentialsAuthorizationHeader = String(format: "Credential=%@", signingCredential)
    let signedHeadersAuthorizationHeader = String(format: "SignedHeaders=%@", AWSSignatureV4Signer.getSignedHeadersString(request.allHTTPHeaderFields))
    let signatureAuthorizationHeader = String(format: "Signature=%@", AWSSignatureSignerUtility.hexEncode(NSString.init(data: signature!, encoding: String.Encoding.ascii.rawValue)! as String))
    let authorization = String(format: "%@ %@, %@, %@", AWSSignatureV4Algorithm,credentialsAuthorizationHeader,signedHeadersAuthorizationHeader,signatureAuthorizationHeader)
    let headers = [
        "Content-Type": "application/json" ,
        "x-amz-security-token" : self.awsCredential.sessionKey ?? "",
        "x-amz-date" : date.full,
        "Content-Length"  : contentLength?.description ?? "0",
        "Authorization" : authorization,
        "Host" : host ?? ""
    ]
    Alamofire.request(request).responseString{ (response: DataResponse<String>) in
        print("\(response.result.value)")
    }
}

Solution

  • The below code is working well for me.

    func getUrlRequest(url:String,params:[String :Any]) -> URLRequest{
    
        var headers = [String :String]()
        var signedHeaders = [String:String]()
    
        var bodyDigest = sha256("")
    
        var urlForSigning = url
        if urlForSigning.last == "/" {
            urlForSigning = String(url.dropLast())
        }
    
        var request = URLRequest(url: URL(string: url)!)
        request.httpMethod = HttpMethod.post.rawValue
    
        let body = try? JSONSerialization.data(withJSONObject: params, options: [])
        let bodyString = NSString(data: body!, encoding: String.Encoding.utf8.rawValue)! as String
        if (bodyString.trim().count > 0){
            bodyDigest = sha256(bodyString)
            request.setValue("application/json", forHTTPHeaderField: "Content-Type")
            request.httpBody = bodyString.data(using: String.Encoding.utf8)
        }
    
        signedHeaders = self.signedHeaders(url: URL(string: urlForSigning)!,
                                             bodyDigest: bodyDigest, httpMethod: HttpMethod.post.rawValue)
    
        headers["Authorization"] = signedHeaders["Authorization"]
        headers["x-amz-date"] = signedHeaders["x-amz-date"]
        headers["Host"] = signedHeaders["Host"]
        headers["expiration"] = signedHeaders["expiration"]
        headers["x-amz-security-token"] = signedHeaders["x-amz-security-token"]
        headers["Content-Type"] = signedHeaders["Content-Type"]
    
        for (k, v) in headers {
            request.setValue(v, forHTTPHeaderField: k)
        }
    
        return request
    }
    
    func signedHeaders(url: URL, bodyDigest: String,
                       httpMethod: String, date: Date = Date()) -> [String: String] {
        let datetime = timestamp(date)
        let expirationTime = timestamp(self.expiration)
        let port = ((url as NSURL).port != nil) ? ":" + String(describing: (url as NSURL).port!) : ""
        var headers = ["x-amz-date": datetime, "Host": url.host! + port,  "expiration": expirationTime, "x-amz-security-token" : self.sessionKey, "Content-Type": "application/json"]
        headers["Authorization"] = authorization(accessKey, secretKey: secretKey, url: url, headers: headers,
                                                 datetime: datetime, httpMethod: httpMethod, bodyDigest: bodyDigest)
    
        return headers
    }
    // MARK: Utilities
    
    fileprivate func pathForURL(_ url: URL) -> String {
        var path = url.path
        if path.isEmpty {
            path = "/"
        } else {
            // do this to preserve encoded path fragments, like those containing encoded '/' (%2F)
            // NSURL.path  decodes them and they are lost
            var encodedPartsArray = [String]()
            // get rid of 'http(s)://'
            let fullURL = String(url.absoluteString[url.absoluteString.index(url.absoluteString.startIndex, offsetBy: 8)...])
            var rawPath = String(fullURL[fullURL.range(of: "/")!.lowerBound...])
            if rawPath.contains("?") {
                rawPath = String(rawPath[..<rawPath.range(of: "?")!.lowerBound])
            }
            for part in rawPath.components(separatedBy: "/") {
                if !part.isEmpty {
                    encodedPartsArray.append(Signer.encodeURIComponent(part))
                }
            }
            path = "/" + encodedPartsArray.joined(separator: "/")
        }
        return path
    }
    
    func sha256(_ str: String) -> String {
        let data = str.data(using: String.Encoding.utf8)!
        return data.sha256().toHexString()
    }
    
    fileprivate func hmac(_ string: NSString, key: Data) -> Data {
        let msg = string.data(using: String.Encoding.utf8.rawValue)!.bytes
        let hmac:[UInt8] = try! HMAC(key: key.bytes, variant: .sha256).authenticate(msg)
        return Data(bytes: hmac)
    }
    
    fileprivate func timestamp(_ date: Date) -> String {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyyMMdd'T'HHmmss'Z'"
        formatter.timeZone = TimeZone(identifier: "UTC")
        formatter.locale = Locale(identifier: "en_US_POSIX")
        return formatter.string(from: date)
    }
    
    // MARK: Methods Ported from AWS SDK
    
    fileprivate func authorization(_ accessKey: String, secretKey: String, url: URL, headers: Dictionary<String, String>,
                                   datetime: String, httpMethod: String, bodyDigest: String) -> String {
        let cred = credential(accessKey, datetime: datetime)
        let shead = signedHeaders(headers)
        let sig = signature(secretKey, url: url, headers: headers, datetime: datetime,
                            httpMethod: httpMethod, bodyDigest: bodyDigest)
    
        return [
            "AWS4-HMAC-SHA256 Credential=\(cred)",
            "SignedHeaders=\(shead)",
            "Signature=\(sig)",
            ].joined(separator: ", ")
    }
    
    fileprivate func credential(_ accessKey: String, datetime: String) -> String {
        return "\(accessKey)/\(credentialScope(datetime))"
    }
    
    fileprivate func signedHeaders(_ headers: [String:String]) -> String {
        var list = Array(headers.keys).map { $0.lowercased() }.sorted()
        if let itemIndex = list.index(of: "authorization") {
            list.remove(at: itemIndex)
        }
        return list.joined(separator: ";")
    }
    
    fileprivate func canonicalHeaders(_ headers: [String: String]) -> String {
        var list = [String]()
        let keys = Array(headers.keys).sorted {$0.localizedCompare($1) == ComparisonResult.orderedAscending}
    
        for key in keys {
            if key.caseInsensitiveCompare("authorization") != ComparisonResult.orderedSame {
                // Note: This does not strip whitespace, but the spec says it should
                list.append("\(key.lowercased()):\(headers[key]!)")
            }
        }
        return list.joined(separator: "\n")
    }
    
    fileprivate func signature(_ secretKey: String, url: URL, headers: [String: String],
                               datetime: String, httpMethod: String, bodyDigest: String) -> String {
        let secret = NSString(format: "AWS4%@", secretKey).data(using: String.Encoding.utf8.rawValue)!
        let date = hmac(String(datetime[..<datetime.index(datetime.startIndex, offsetBy: 8)]) as NSString, key: secret)
        let region = hmac(regionName as NSString, key: date)
        let service = hmac(serviceName as NSString, key: region)
        let credentials = hmac("aws4_request", key: service)
        let string = stringToSign(datetime, url: url, headers: headers, httpMethod: httpMethod, bodyDigest: bodyDigest)
        return hmac(string as NSString, key: credentials).toHexString()
    }
    
    fileprivate func credentialScope(_ datetime: String) -> String {
        return [
            String(datetime[..<datetime.index(datetime.startIndex, offsetBy: 8)]),
            regionName,
            serviceName,
            "aws4_request"
            ].joined(separator: "/")
    }
    
    fileprivate func stringToSign(_ datetime: String, url: URL, headers: [String: String],
                                  httpMethod: String, bodyDigest: String) -> String {
        return [
            "AWS4-HMAC-SHA256",
            datetime,
            credentialScope(datetime),
            sha256(canonicalRequest(url, headers: headers, httpMethod: httpMethod, bodyDigest: bodyDigest)),
            ].joined(separator: "\n")
    }
    
    fileprivate func canonicalRequest(_ url: URL, headers: [String: String],
                                      httpMethod: String, bodyDigest: String) -> String {
        return [
            httpMethod,                       // HTTP Method
            pathForURL(url),                  // Resource Path
            url.query ?? "",                  // Canonicalized Query String
            "\(canonicalHeaders(headers))\n", // Canonicalized Header String (Plus a newline for some reason)
            signedHeaders(headers),           // Signed Headers String
            bodyDigest,                       // Sha265 of Body
            ].joined(separator: "\n")
    }
    
    open static func encodeURIComponent(_ s: String) -> String {
        let allowed = NSMutableCharacterSet.alphanumeric()
        allowed.addCharacters(in: "-_.~")
        //allowed.addCharactersInString("-_.!~*'()")
        return s.addingPercentEncoding(withAllowedCharacters: allowed as CharacterSet) ?? ""
    }