/S3SignerAWS

Pure Swift AWS S3 Signer, generates V4 Auth Headers and V4/V2 presignedURLs

Primary LanguageSwiftMIT LicenseMIT

S3SignerAWS

Generates V4 authorization headers and pre-signed URLs for authenticating AWS S3 REST API calls

Update for Vapor Users: Check out VaporS3Signer a Provider for easy integration with your Vapor server.

I wrote the majority of this for personal use on the Vapor Server and found getting the signatures to work in a flexible and reusable way pretty painful and tedious. Hopefully this will save others some time. I tried to expand the uses beyond my specific needs, but this does not cover all use cases. It does not cover chunked-uploads or POST requests(On AWS this is used for adding an object to a bucket using HTML forms). Anyone who would like to contribute is more than welcome.

NOTE - I tried to test this as much as possible, within reason, but It's hard to be sure with so many options and the signing process being so particular. If you encounter a bug, open up an issue and I'll be happy to look into it.

Features

  1. Pure Swift
  2. All required headers generated automatically
  3. Authorization methods currently supported:
  • V4 Authorization header
    • Supports PUT/GET/DELETE
  • V4 pre-signed URL
  • V2 pre-signed URL

Table of Contents

Integration

Swift Package Manager

To install with swift package manager, add the package to your Package.swift file:

      Import PackageDescription

      let package = Package(
        name: "Your_Project_Name",
        targets: [],
        dependencies: [
            .Package(url: "https://github.com/JustinM1/S3SignerAWS.git", majorVersion: 1)
        ]
      )  

Usage

NOTE ABOUT PAYLOAD:

Requests can either have a signed payload or an unsigned payload.

  • S3SignerAWS was built using Vapor Server frameworks and uses Bytes as the payload data type. Bytes is a typealias of [UInt8] For example, to convert a data object to bytes you do the following:
      let bytes = try someDataObject.makeBytes()
  • If you know the request will not have a payload, set the Payload property to none. This tells s3 that the signature was created with no payload intended

  • If you are not sure what the exact payload will be, set payload property to unsigned. This tells s3 when you made the signature, there was a possibility of a payload but you weren't sure what specific object will be uploaded.

  • Payload enum:

        public enum Payload {
          case bytes(Bytes)
          case none
          case unsigned
        }

To begin working with the S3SignerAWS class, initialize an instance similar to example shown below:

    let s3Signer = S3SignerAWS(accessKey: "YOUR_AWS_PUBLIC_KEY", secretKey: "YOUR_AWS_SECRET_KEY", region: .usStandard_usEast1)  

NOTE - Hardcoding Secret Keys on client is not recommended for security reasons.

V4 Authorization Header

For both V4 Authorization Header and Pre-Signed URL, you can add additional headers as needed for your specific use case.

GET

    do {
      let headers = try s3Signer.authHeaderV4(httpMethod: .get, urlString: "https://s3.amazonaws.com/bucketName/testUploadImage.png", headers: [:], payload: .none)          
      guard let url = URL(string: "https://s3.amazonaws.com/bucketName/testUploadImage.png") else { throw someError }
      var request = URLRequest(url: url)
      request.httpMethod = HTTPMethod.get.rawValue
        for header in headers {
          request.setValue(header.key, forHTTPHeaderField: header.value)
          }
          // make network request
        } catch {
          //handle error
          }
        }

PUT

    do {
      let bytesObject = try someDataObject.makeBytes()
      let headers = try s3Signer.authHeadersV4(httpMethod: .put, urlString: "https://s3.amazonaws.com/bucketName/testUploadImage.png", headers: [:], payload: .bytes(bytesObject))
      guard let url = URL(string: "https://s3.amazonaws.com/bucketName/testUploadImage.png") else { throw someError }
      var request = URLRequest(url: url)
      request.httpMethod = HTTPMethod.put.rawValue
      request.httpBody = Data(bytes: bytesObject)
        for header in headers {
          request.setValue(header.key, forHTTPHeaderField: header.value)
          }
          // make network request
        } catch {
          //handle error
          }
        }

DELETE

    do {
      let headers = try s3Signer.authHeadersV4(httpMethod: .delete, urlString: "https://s3.amazonaws.com/bucketName/testUploadImage.png", headers: [:], payload: .none)
      guard let url = URL(string: "https://s3.amazonaws.com/bucketName/testUploadImage.png") else { throw someError }
      var request = URLRequest(url: url)
      request.httpMethod = HTTPMethod.delete.rawValue
        for header in headers {
          request.setValue(header.key, forHTTPHeaderField: header.value)
          }
          // make network request
        } catch {
          //handle error
        }
      }

V4 Pre-Signed URL

Similar to the ease of generating authentication headers, to generate a pre-signed url:

      let presignedURL = signer.presignedURLV4(httpMethod: HTTPMethod, urlString: String, expiration: TimeFromNow, headers: [String:String]) -> URLV4Returnable

      let urlString = presignedURL.urlString
      let headers = presignedURL.headers

V2 Pre-Signed URL

V4 is the only authorization accepted by all s3 regions, however, since I had the implementation complete I decided to include V2 as well.

    let presignedURL = presignedURLV2(httpMethod: HTTPMethod, contentType:String, urlString: String, expiration: TimeFromNow) throws -> String

Known Limitations

  • bucket names that contain "-" may get a 403 response("Signature does not match") from AWS.