AWS Open Source Blog

Introducing the AWS SigV4 Signer for Dart

The AWS Amplify team is pleased to announce the availability of the AWS SigV4 Signer for Dart, an open source implementation of the AWS Signature Version 4 protocol in the Dart programming language. The SigV4 signer is a library which developers can include in their projects to sign and send HTTP requests to AWS services using their AWS credentials. Check it out on pub.dev.

AWS Amplify is a set of tools and services that enables mobile and front-end web developers to build secure, scalable full stack applications powered by AWS. It consists of three main components: a set of open source libraries and UI components for adding cloud-powered functionalities, a CLI toolchain to create and manage cloud backends, and AWS Amplify Studio, a service to deploy and host full stack, serverless applications.

Flutter is Google’s UI toolkit for building beautiful, natively compiled applications for mobile, web, and desktop from a single codebase using the Dart programming language. AWS Amplify supports building full stack Flutter applications using the Amplify Flutter client libraries and CLI. While we continue to expand support for more AWS services in Amplify Flutter, the SigV4 signer provides developers a way to build custom features and integrations that interact directly with these AWS services before they’re supported out of the box.

Example

To see the signer in action, let’s walk through the creation of a bucket in Amazon Simple Storage Service (Amazon S3) and how you might upload and share links to files. The complete code for all the steps in this example is available here. We’ll start by creating a bucket and uploading a file to it. Then, we’ll look at how we can generate “pre-signed” URLs which we can share granting access to the file for a limited period of time.

Note: Following along will require access to AWS credentials with the following permissions:

s3:CreateBucket
s3:PutObject
s3:GetObject

While objects uploaded to buckets are private by default and require your credentials to access, that does not mean your buckets are guaranteed to be secure. Review the AWS documentation for securing buckets and/or consult a security expert before using Amazon S3 buckets in production.

Getting Started

To get started using the SigV4 signer, add it as a dependency in your pubspec.yaml,

dependencies:
aws_common: ^0.2.0
aws_signature_v4: ^0.2.0

Then create an instance in your project.

import 'package:aws_signature_v4/aws_signature_v4.dart';

const signer = AWSSigV4Signer();

Credentials are configured in the signer by overriding the credentialsProvider parameter of the constructor. By default, the signer pulls credentials from your environment via the AWSCredentialsProvider.environment() provider. On mobile and web, this means using the Dart environment, which is configured by passing the --dart-define flag to your flutter commands, as below.

flutter run --dart-define=AWS_ACCESS_KEY_ID=... --dart-define=AWS_SECRET_ACCESS_KEY=... .

On desktop, credentials are retrieved from the system’s environment using Platform.environment.

Take care when handling credentials.

To retrieve credentials via another mechanism, pass a credentialsProvider object to the signer’s constructor. There are a few available out of the box, but any class conforming to the AWSCredentialsProvider interface can be used.

Creating a bucket

To create a bucket, we need to be able to call Amazon S3’s CreateBucket API. Amazon S3 uses a REST protocol and serializes its messages using XML. Since Dart does not provide XML support out of the box, we’ll write our requests by hand as strings — although, for larger requests, consider using a community package like xml.

Every signer action begins the same way: by defining the scope of the credentials it produces. This is broken down into three components: the ID of the service being accessed, the region of the service being accessed, and the time of the request (by default, the current time). For our requests to Amazon S3, that looks like the following:

const region = 'us-west-2'; // Choose any region you'd like
final scope = AWSCredentialScope(region: region, service: 's3');

A list of supported regions for Amazon S3 can be found here.

We can reuse this scope in all of the requests in this example since they will be executed within the same window of time Signer outputs are only valid for 15 minutes after the time of the scope, so generally it is better to create a new scope for each request.

We’ll give our bucket a name and use virtual hosting to interact with it (even though it does not exist yet). You can choose any bucket name you’d like, but remember that Amazon S3 bucket names must be globally unique across all Amazon S3 buckets. For this example, we append a random integer to our bucket name to ensure uniqueness.

import 'dart:math';

final bucketName = 'mybucket-${Random().nextInt(1 << 30)}';
final host = '$bucketName.s3.$region.amazonaws.com';

Also, unlike many other services, Amazon S3 has some specifics when it comes to signing. The signing package encapsulates that logic for us by using service configurations.

If you want to learn more about signing details, you can check the details here.

final serviceConfiguration = S3ServiceConfiguration();

Now we’re ready to create an HTTP request which can be passed to the signer for signing. Following the request syntax described in the API docs, we construct an AWSHttpRequest and pass in our XML body as a UTF-8 encoded string.

If you’re familiar with the request/response types of the HTTP package, the AWS types are meant to mirror these. There are AWSHttpRequest/Response types as well as AWSStreamedHttpRequest/Response counterparts for streaming requests.

Amazon S3 expects a few headers to be present in our request, most notably the Host header, which is required by most AWS services. It also expects that the Content-Length and Content-Type headers are present, so we include those here.

import 'dart:convert';

import 'package:aws_common/aws_common.dart';

// The body of the request, which specifies the bucket's location
final createBody = utf8.encode('''
<CreateBucketConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
  <LocationConstraint>$region</LocationConstraint>
</CreateBucketConfiguration>
''');

// Create a bucket by making a PUT request to the root of the bucket
final createRequest = AWSHttpRequest(
  method: HttpMethod.put,
  host: host,
  path: '/',
  body: createBody,
  headers: {
    AWSHeaders.host: host,
    
    // Remember to include the content length and content type
    // for XML requests to S3
    AWSHeaders.contentLength: createBody.length.toString(),
    AWSHeaders.contentType: 'application/xml',
  },
);

To sign the request, we simply pass it to the signer, getting back a new HTTP request with the required headers needed to authorize us to AWS servers.

// Sign and send the PUT request
final signedCreateRequest = await signer.sign(
  createRequest,
  credentialScope: scope,
  serviceConfiguration: serviceConfiguration,
);

We can send the request by calling send.

final createResponse = await signedCreateRequest.send();

Finally, we check to make sure it was successful. Since bucket names must be unique, we’ll get an error if we happened to pick a name which is already in use. In this case, Amazon S3 returns the 409 or Conflict status code. Any other code besides 200 means our request failed in some way.

final createStatus = createResponse.statusCode;
print('Create Bucket Response: $createStatus');

if (createStatus == 409) {
  print('Bucket name already exists!');
} else if (createStatus != 200) {
  print('Bucket creation failed');
}

A list of all error codes can be found here.

Uploading a file

Assuming you got a 200 status code from the create request, we’re ready to upload a file to our bucket. If not, consult the Amazon S3 docs for help troubleshooting the error code you received.

To upload a file to our bucket, we need to call the PutObject API, which expects a PUT request to the file’s location in the bucket with the contents of the file as the body. We can open a file for signing by calling the openRead method on a File instance. We’ll place the file at the root of our bucket by assigning it a bucket key of /$filename — this will become the path of our HTTP request to upload the file.

// Create a file and write some contents to it. Then, open it 
// for reading.
const filename = 'myfile.txt';
final file = File(filename)..writeStringSync('my file');
final contents = file.openRead();
const key = '/$filename';

The process from here is the same as before, except that this time we use the AWSStreamedHttpRequest constructor since we are working directly with a Stream now. We also leave out the Content-Length header, since the signer will add it automatically for us if not included.

// Create a PUT request to the path of the file.
final uploadRequest = AWSStreamedHttpRequest(
  method: HttpMethod.put,
  host: host,
  path: key,
  body: contents,
  headers: {
    AWSHeaders.host: host,
    AWSHeaders.contentType: 'text/plain',
  },
);

// Sign and send the upload request
final signedUploadRequest = await signer.sign(
  uploadRequest,
  credentialScope: scope,
  serviceConfiguration: serviceConfiguration,
);
final uploadResponse = await signedUploadRequest.send();
final uploadStatus = uploadResponse.statusCode;
print('Upload File Response: $uploadStatus');
if (uploadStatus != 200) {
  print('Could not upload file');
}

Creating a pre-signed URL

Now that we’ve uploaded the file, maybe we need to send a link for others to access it, or maybe one of our own applications needs to display its contents. In order to do so, we can create a pre-signed URL which allows anyone with the URL to access the contents of the file for a preset period of time (between 1 second and 7 days).

In order to do so, we’ll sign a GET request for the URL of the file, but instead of calling signer.sign, we’ll use signer.presign which will attach the credentials as query parameters to the URL instead of as headers. This means that only the URL will be needed to access the file.

// Construct the URI we'd like to sign as an HTTP request
final urlRequest = AWSHttpRequest(
  method: HttpMethod.get,
  host: host,
  path: key,
  headers: {
    AWSHeaders.host: host,
  },
);

// Use the signer's presign function to create a signed URI
final signedUrl = await signer.presign(
  urlRequest,
  credentialScope: scope,
  serviceConfiguration: serviceConfiguration,
  expiresIn: const Duration(minutes: 10),
);
print('Download URL: $signedUrl');

This will print a long URL, which looks like the one below, that can be used as many times as needed until its expiration.

https://mybucket-754375478539575.s3.us-west-2.amazonaws.com/myfile.txt?X-Amz-Date=20220328T210444Z&X-Amz-SignedHeaders=host&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAR7FIPFYKSVQ32MET%2F20220328%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Expires=600&X-Amz-Signature=a90c8b9df4f50458513b7f4883dd1c1203f2753d565a3680a81c3164e3936b5a

Wrapping up

Command line gif of S3 bucket creation using Dart
We’ve walked through the process of creating an Amazon S3 bucket, uploading a file to it and sharing access to that file via pre-signed URLs.

Make sure to delete the file and bucket from Amazon S3 using the AWS console or the AWS CLI if you no longer need it.

The Dart V4 signer is the first step that the Amplify Flutter team is taking towards expanding platform support using Dart, unlocking web and desktop support for Amplify Flutter developers.

We’re excited to bring this bit of functionality to the Dart ecosystem and look forward to seeing how people incorporate the signer into their Dart and Flutter projects. Let us know what you’re building next on Discord. Feel free to leave any feedback on our GitHub issues board and any of the active community discussions around web and desktop support. Review our open request for comments (RFC) for updates to the API category.

Dillon Nys

Dillon Nys

Dillon Nys is a software engineer at AWS. He focuses on development of the Amplify Flutter libraries and building pathways for developers to write Dart applications on AWS.

Abdallah Shaban

Abdallah Shaban

Abdallah Shaban is a Senior Product Manager at AWS Amplify, helping Javascript and Flutter developers create apps that delight their users. When not working, Abdallah tries to keep himself updated on the newest innovations in tech, playing his guitar, and traveling.

Ashish Nanda

Ashish Nanda

Ashish Nanda is a Senior Software Engineer and Tech Lead at AWS Amplify. He leads design and engineering on the JavaScript and Flutter open source SDK teams with the goal of helping developers build full-stack web and mobile applications quickly and seamlessly using cloud services.