diff --git a/flysystem_s3.js b/flysystem_s3.js
new file mode 100644
index 0000000..3ec5f06
--- /dev/null
+++ b/flysystem_s3.js
@@ -0,0 +1,192 @@
+/**
+ * @file
+ * Provides JavaScript additions to the S3 CORS upload managed file field type.
+ */
+
+(function ($, Drupal) {
+
+  'use strict';
+
+  /**
+   * File upload utility functions.
+   *
+   * @namespace
+   */
+  Drupal.flysystemS3 = Drupal.flysystemS3 || {
+
+    /**
+     * Submit file via CORS.
+     *
+     * @name Drupal.flysystemS3.handleCorsUpload
+     *
+     * @param {jQuery.Event} event
+     *   The event triggered, most likely a `mousedown` event.
+     */
+    submitCorsUpload: function (event) {
+      var $fileElement = $(event.target).siblings('input[type="file"]');
+
+      // @todo This only supports the first file uploaded. Batch all the files together?
+      if (typeof $fileElement[0].files !== 'undefined') {
+        var file = $fileElement[0].files[0];
+        var $form = $(event.target).parents('form');
+        var field_id = '#' + $fileElement.attr('id').replace(/\-upload$/, '');
+        var form_build_id = $form.find('input[name="form_build_id"]').val();
+
+        // Prevent the submit button from actually submitting.
+        event.preventDefault();
+
+        // Add client side validation for the input[type=file].
+        // @todo Figure out why Drupal.file.validateExtension is not getting triggered.
+        if (typeof drupalSettings.file.elements[field_id] !== 'undefined') {
+          $('.file-upload-js-error').remove();
+          var extensionPattern = drupalSettings.file.elements[field_id].replace(/,\s*/g, '|');
+          if (extensionPattern.length > 1 && file.name.length > 0) {
+            var acceptableMatch = new RegExp('\\.(' + extensionPattern + ')$', 'gi');
+            if (!acceptableMatch.test(file.name)) {
+              var error = Drupal.t('The selected file %filename cannot be uploaded. Only files with the following extensions are allowed: %extensions.', {
+                '%filename': file.name,
+                '%extensions': extensionPattern.replace(/\|/g, ', ')
+              });
+              $(this).closest('div.js-form-managed-file').prepend('<div class="messages messages--error file-upload-js-error" aria-live="polite">' + error + '</div>');
+              // Cancel all other submit event handlers.
+              event.stopImmediatePropagation();
+              return;
+            }
+          }
+        }
+
+        // Add a progress bar.
+        var $progressBar = $(Drupal.theme.progressBar(field_id + '-progress'));
+        $fileElement.hide();
+        $fileElement.siblings('.description').hide();
+        $fileElement.after($progressBar);
+
+        // Use the file object and ask Drupal to generate the appropriate signed
+        // request for us.
+        var signingPostData = {
+          filename: file.name,
+          filesize: file.size,
+          filemime: file.type,
+          acl: $fileElement.attr('data-s3-acl'),
+          destination: $fileElement.attr('data-s3-destination')
+        };
+
+        // POST to Drupal which will return the required parameters for signing
+        // a CORS request.
+        $.ajax({
+          url: drupalSettings.path.baseUrl + 'flysystem-s3/cors-upload-sign',
+          data: signingPostData,
+          type: 'POST',
+          success: function (signedFormData) {
+
+            // Take the signed data and construct a form out of it.
+            var uploadFormData = new FormData();
+            $.each(signedFormData.inputs, function(key, value) {
+              uploadFormData.append(key, value);
+            });
+
+            // Add the file to be uploaded.
+            uploadFormData.append('file', file);
+
+            $.ajax({
+              url: signedFormData.attributes.action,
+              data: uploadFormData,
+              type: signedFormData.attributes.method,
+              mimeType: signedFormData.attributes.enctype,
+              xhrFields: {
+                withCredentials: true
+              },
+              cache: false,
+              contentType: false,
+              processData: false,
+              xhr: function () {
+                var myXhr = $.ajaxSettings.xhr();
+                if (myXhr.upload) {
+                  myXhr.upload.addEventListener('progress', (function (event) {
+                    Drupal.flysystemS3.displayCorsUploadProgress($progressBar, event);
+                  }), false);
+                }
+                return myXhr;
+              },
+              error: function (data) {
+                // todo: deal w/ upload errors.
+              },
+              complete: function (data) {
+                // Update the hidden fields to tell Drupal about the file that
+                // was just uploaded.
+                //var button_id = $(event.target).attr('data-drupal-selector');
+                //var ajaxSettings = drupalSettings.ajax[button_id];
+                //Drupal.ajax(ajaxSettings).execute();
+
+                /*$file.parent().find('input[name$="[filemime]"]').val(f.type);
+                 $file.parent().find('input[name$="[filesize]"]').val(f.size);
+                 // Make sure and use the filename provided by Drupal as it may have
+                 // been renamed.
+                 $file.parent().find('input[name$="[filename]"]').val(data.file_real);
+                 // Re-enable all the submit buttons.
+                 $form.find('input[type="submit"]').removeAttr('disabled');
+                 // Find trigger the #ajax method for the upload button that was
+                 // initially clicked to upload the file.
+                 var button_id = $file.parent().find('input.cors-form-submit').attr('id');
+                 ajax = Drupal.ajax[button_id];
+                 // Prevent Drupal from transferring the file twice as part of the
+                 // form rebuild.
+                 var file_selector_id = $file.attr('id');
+                 $(ajax.form[0]).find('#' + file_selector_id).remove();
+
+                 ajax.form.ajaxSubmit(ajax.options);*/
+              }
+            });
+          }
+        });
+      }
+      else {
+        //$form.submit();
+      }
+    },
+
+    /**
+     * Receives an XMLHttpRequestProgressEvent and uses it to display current
+     * progress if possible.
+     *
+     * @param $element
+     *
+     * @param event
+     *   And XMLHttpRequestProgressEvent object.
+     */
+    displayCorsUploadProgress: function($element, event) {
+      if (event.lengthComputable) {
+        // This is copied mostly from  Drupal.ProgressBar.setProgress
+        var percentage = Math.floor((event.loaded / event.total) * 100);
+        if (percentage >= 0 && percentage <= 100) {
+          $element.find('div.progress__bar').css('width', percentage + '%');
+          $element.find('div.progress__percentage').html(percentage + '%');
+        }
+        return true;
+      }
+    }
+
+  };
+
+  /**
+   * Attach behaviors to submit uploads via CORS.
+   *
+   * @type {Drupal~behavior}
+   *
+   * @prop {Drupal~behaviorAttach} attach
+   *   Attaches triggers for the upload button.
+   * @prop {Drupal~behaviorDetach} detach
+   *   Detaches auto file upload trigger.
+   */
+  Drupal.behaviors.flySystemS3CorsUpload = {
+    attach: function (context) {
+      var $context = $(context);
+      $context.find('.js-form-type-flysystem-s3-cors-managed-file .js-form-submit').once('flysystem-s3-cors-file-upload').on('mousedown', Drupal.flysystemS3.submitCorsUpload);
+    },
+    detach: function (context) {
+      var $context = $(context);
+      $context.find('.js-form-type-flysystem-s3-cors-managed-file .js-form-submit').off('mousedown', Drupal.flysystemS3.submitCorsUpload);
+    }
+  };
+
+})(jQuery, Drupal, drupalSettings);
diff --git a/flysystem_s3.libraries.yml b/flysystem_s3.libraries.yml
new file mode 100644
index 0000000..f236196
--- /dev/null
+++ b/flysystem_s3.libraries.yml
@@ -0,0 +1,7 @@
+drupal.s3_cors_upload:
+  version: VERSION
+  js:
+    flysystem_s3.js: {}
+  dependencies:
+    - file/drupal.file
+    - core/drupal.progress
diff --git a/flysystem_s3.permissions.yml b/flysystem_s3.permissions.yml
new file mode 100644
index 0000000..b7c9702
--- /dev/null
+++ b/flysystem_s3.permissions.yml
@@ -0,0 +1,2 @@
+use S3 CORS upload:
+  title: 'Upload files directly to S3 using CORS'
diff --git a/flysystem_s3.routing.yml b/flysystem_s3.routing.yml
new file mode 100644
index 0000000..6bc7cb5
--- /dev/null
+++ b/flysystem_s3.routing.yml
@@ -0,0 +1,7 @@
+flysystem_s3.cors:
+  path: '/flysystem-s3/cors-upload-sign'
+  defaults:
+    _controller: '\Drupal\flysystem_s3\Controller\S3CorsUploadAjaxController::signRequest'
+  requirements:
+    _permission: 'use S3 CORS upload'
+    _method: 'POST'
diff --git a/src/Controller/S3CorsUploadAjaxController.php b/src/Controller/S3CorsUploadAjaxController.php
new file mode 100644
index 0000000..abf441c
--- /dev/null
+++ b/src/Controller/S3CorsUploadAjaxController.php
@@ -0,0 +1,99 @@
+<?php
+
+namespace Drupal\flysystem_s3\Controller;
+
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\flysystem\FlysystemFactory;
+use Drupal\Core\File\FileSystemInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\JsonResponse;
+use Symfony\Component\HttpFoundation\Request;
+use Aws\S3\PostObjectV4;
+
+/**
+ * Defines a controller to respond to S3 CORS upload AJAX requests.
+ */
+class S3CorsUploadAjaxController extends ControllerBase {
+
+  /**
+   * The form builder.
+   *
+   * @var \Drupal\flysystem\FlysystemFactory
+   */
+  protected $flysystemFactory;
+
+  /**
+   * The file system.
+   *
+   * @var \Drupal\Core\File\FileSystemInterface
+   */
+  protected $fileSystem;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('flysystem_factory'),
+      $container->get('file_system')
+    );
+  }
+
+  /**
+   * Constructs an S3CorsUploadAjaxController object.
+   *
+   * @param \Drupal\flysystem\FlysystemFactory $flysystem_factory
+   *   The Flysystem factory.
+   * @param \Drupal\Core\File\FileSystemInterface $file_system
+   *   The file system.
+   */
+  public function __construct(FlysystemFactory $flysystem_factory, FileSystemInterface $file_system) {
+    $this->flysystemFactory = $flysystem_factory;
+    $this->fileSystem = $file_system;
+  }
+
+  /**
+   * Returns the signed request.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The current request.
+   *
+   * @return \Symfony\Component\HttpFoundation\JsonResponse
+   *   A JsonResponse object.
+   */
+  public function signRequest(Request $request) {
+    $post = $request->request->all();
+
+    /** @var \Drupal\flysystem_s3\Flysystem\Adapter\S3Adapter $adapter */
+    $scheme = $this->fileSystem->uriScheme($post['destination']);
+    $adapter = $this->flysystemFactory->getPlugin($scheme)->getAdapter();
+
+    $client = $adapter->getClient();
+    $bucket = $adapter->getBucket();
+
+    $options = [
+      ['acl' => $post['acl']],
+      ['bucket' => $bucket],
+      ['starts-with', '$key', file_uri_target($post['destination']) . '/'],
+    ];
+
+    $post['key'] = file_uri_target(file_create_filename($post['filename'], $post['destination']));
+
+    unset($post['destination']);
+    unset($post['filename']);
+    unset($post['filemime']);
+    unset($post['filesize']);
+
+    // @todo Make this interval configurable.
+    $expiration = '+5 hours';
+    $postObject = new PostObjectV4($client, $bucket, $post, $options, $expiration);
+
+    $data = [];
+    $data['attributes'] = $postObject->getFormAttributes();
+    $data['inputs'] = $postObject->getFormInputs();
+    $data['options'] = $options;
+
+    return new JsonResponse($data);
+  }
+
+}
diff --git a/src/Element/S3CorsManagedFile.php b/src/Element/S3CorsManagedFile.php
new file mode 100644
index 0000000..393d2a6
--- /dev/null
+++ b/src/Element/S3CorsManagedFile.php
@@ -0,0 +1,196 @@
+<?php
+
+namespace Drupal\flysystem_s3\Element;
+
+use Drupal\Component\Utility\Crypt;
+use Drupal\Component\Utility\Html;
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\ReplaceCommand;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Render\Element\FormElement;
+use Drupal\Core\Site\Settings;
+use Drupal\Core\Url;
+use Drupal\file\Element\ManagedFile;
+use Drupal\file\Entity\File;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Provides an AJAX/progress aware widget for uploading and saving a file.
+ *
+ * @FormElement("flysystem_s3_cors_managed_file")
+ */
+class S3CorsManagedFile extends ManagedFile {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getInfo() {
+    $class = get_called_class();
+    return [
+      '#input' => TRUE,
+      '#process' => [
+        [$class, 'processManagedFile'],
+      ],
+      '#element_validate' => [
+        [$class, 'validateManagedFile'],
+      ],
+      '#pre_render' => [
+        [$class, 'preRenderManagedFile'],
+      ],
+      '#theme' => 'file_managed_file',
+      '#theme_wrappers' => ['form_element'],
+      '#progress_indicator' => 'bar',
+      '#progress_message' => NULL,
+      '#upload_validators' => [],
+      '#upload_location' => NULL,
+      '#size' => 22,
+      '#multiple' => FALSE,
+      '#extended' => FALSE,
+      '#attached' => [
+        'library' => ['flysystem_s3/drupal.s3_cors_upload'],
+      ],
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
+    // Find the current value of this field.
+    $fids = !empty($input['fids']) ? explode(' ', $input['fids']) : [];
+    foreach ($fids as $key => $fid) {
+      $fids[$key] = (int) $fid;
+    }
+    $force_default = FALSE;
+
+    // Process any input and save new uploads.
+    if ($input !== FALSE) {
+      $input['fids'] = $fids;
+      $return = $input;
+
+      // Uploads take priority over all other values.
+      if ($files = file_managed_file_save_upload($element, $form_state)) {
+        if ($element['#multiple']) {
+          $fids = array_merge($fids, array_keys($files));
+        }
+        else {
+          $fids = array_keys($files);
+        }
+      }
+      else {
+        // Check for #filefield_value_callback values.
+        // Because FAPI does not allow multiple #value_callback values like it
+        // does for #element_validate and #process, this fills the missing
+        // functionality to allow File fields to be extended through FAPI.
+        if (isset($element['#file_value_callbacks'])) {
+          foreach ($element['#file_value_callbacks'] as $callback) {
+            $callback($element, $input, $form_state);
+          }
+        }
+
+        // Load files if the FIDs have changed to confirm they exist.
+        if (!empty($input['fids'])) {
+          $fids = [];
+          foreach ($input['fids'] as $fid) {
+            if ($file = File::load($fid)) {
+              $fids[] = $file->id();
+              // Temporary files that belong to other users should never be
+              // allowed.
+              if ($file->isTemporary()) {
+                if ($file->getOwnerId() != \Drupal::currentUser()->id()) {
+                  $force_default = TRUE;
+                  break;
+                }
+                // Since file ownership can't be determined for anonymous users,
+                // they are not allowed to reuse temporary files at all. But
+                // they do need to be able to reuse their own files from earlier
+                // submissions of the same form, so to allow that, check for the
+                // token added by $this->processManagedFile().
+                elseif (\Drupal::currentUser()->isAnonymous()) {
+                  $token = NestedArray::getValue($form_state->getUserInput(), array_merge($element['#parents'], array('file_' . $file->id(), 'fid_token')));
+                  if ($token !== Crypt::hmacBase64('file-' . $file->id(), \Drupal::service('private_key')->get() . Settings::getHashSalt())) {
+                    $force_default = TRUE;
+                    break;
+                  }
+                }
+              }
+            }
+          }
+          if ($force_default) {
+            $fids = [];
+          }
+        }
+      }
+    }
+
+    // If there is no input or if the default value was requested above, use the
+    // default value.
+    if ($input === FALSE || $force_default) {
+      if ($element['#extended']) {
+        $default_fids = isset($element['#default_value']['fids']) ? $element['#default_value']['fids'] : [];
+        $return = isset($element['#default_value']) ? $element['#default_value'] : ['fids' => []];
+      }
+      else {
+        $default_fids = isset($element['#default_value']) ? $element['#default_value'] : [];
+        $return = ['fids' => []];
+      }
+
+      // Confirm that the file exists when used as a default value.
+      if (!empty($default_fids)) {
+        $fids = [];
+        foreach ($default_fids as $fid) {
+          if ($file = File::load($fid)) {
+            $fids[] = $file->id();
+          }
+        }
+      }
+    }
+
+    $return['fids'] = $fids;
+    return $return;
+  }
+
+  /**
+   * Render API callback: Expands the managed_file element type.
+   *
+   * Expands the file type to include Upload and Remove buttons, as well as
+   * support for a default value.
+   */
+  public static function processManagedFile(&$element, FormStateInterface $form_state, &$complete_form) {
+    $use_cors = \Drupal::currentUser()->hasPermission('use S3 CORS upload');
+
+    if ($use_cors) {
+      // Disable the default progress indicator.
+      $element['#progress_indicator'] = 'none';
+    }
+
+    $element = parent::processManagedFile($element, $form_state, $complete_form);
+
+    if ($use_cors) {
+      $element['#attributes']['class'][] = 'flysystem-s3-cors';
+
+      // If a specific ACL has not been set on this upload, fetch the default
+      // ACL from the Flysystem scheme.
+      if (!isset($element['#acl'])) {
+        $scheme = file_uri_scheme($element['#upload_location']);
+        /** @var \Drupal\flysystem\FlysystemFactory $factory */
+        $flysystemFactory = \Drupal::service('flysystem_factory');
+        $scheme_settings = $flysystemFactory->getSettings($scheme);
+        if (isset($scheme_settings['config']['options']['ACL'])) {
+          $element['#acl'] = $scheme_settings['config']['options']['ACL'];
+        }
+        else {
+          $element['#acl'] = 'private';
+        }
+      }
+
+      // The file upload field itself.
+      $element['upload']['#attributes']['data-s3-acl'] = $element['#acl'];
+      $element['upload']['#attributes']['data-s3-destination'] = $element['#upload_location'];
+    }
+
+    return $element;
+  }
+
+}
diff --git a/src/Plugin/Field/FieldWidget/S3CorsUploadWidget.php b/src/Plugin/Field/FieldWidget/S3CorsUploadWidget.php
new file mode 100644
index 0000000..e671f09
--- /dev/null
+++ b/src/Plugin/Field/FieldWidget/S3CorsUploadWidget.php
@@ -0,0 +1,113 @@
+<?php
+
+namespace Drupal\flysystem_s3\Plugin\Field\FieldWidget;
+
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\Field\WidgetBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\Render\Element;
+use Drupal\Core\Render\ElementInfoManagerInterface;
+use Drupal\Core\StreamWrapper\StreamWrapperInterface;
+use Drupal\file\Element\ManagedFile;
+use Drupal\file\Entity\File;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\Validator\ConstraintViolationListInterface;
+use Drupal\file\Plugin\Field\FieldWidget\FileWidget;
+use Drupal\flysystem_s3\Flysystem\S3;
+use Aws\S3\S3Client;
+use Aws\S3\S3ClientInterface;
+
+/**
+ * Plugin implementation of the 's3_cors_upload' widget.
+ *
+ * @FieldWidget(
+ *   id = "s3_cors_upload",
+ *   label = @Translation("S3 CORS File Upload"),
+ *   field_types = {
+ *     "file",
+ *     "image"
+ *   }
+ * )
+ */
+class S3CorsUploadWidget extends FileWidget {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function isApplicable(FieldDefinitionInterface $field_definition) {
+    // The file field must be using the S3 upload scheme.
+    $scheme = $field_definition->getFieldStorageDefinition()
+      ->getSetting('uri_scheme');
+    return static::isSchemeS3($scheme);
+  }
+
+
+  public static function isSchemeS3($scheme) {
+    // There can be multiple S3 schemes, so check the flysystem settings.
+    /** @var \Drupal\flysystem\FlysystemFactory $factory */
+    $flysystemFactory = \Drupal::service('flysystem_factory');
+    $settings = $flysystemFactory->getSettings($scheme);
+    return !empty($settings['driver']) && $settings['driver'] === 's3';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function defaultSettings() {
+    $settings = [];
+    $settings['s3_acl'] = 'private';
+    return $settings;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function settingsForm(array $form, FormStateInterface $form_state) {
+    $element = [];
+
+    $element['s3_acl'] = [
+      '#type' => 'select',
+      '#title' => $this->t('ACL'),
+      '#description' => $this->t('In addition to whatever options you select here the object owner will always be granted full control of all objects. See the <a href="http://docs.aws.amazon.com/AmazonS3/latest/dev/ACLOverview.html">API documentation</a> for more information.'),
+      '#options' => [
+        // http://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html
+        'private' => $this->t('No one else has access. (private)'),
+        'public-read' => $this->t('All users get READ access. (public-read)'),
+        'public-read-write' => $this->t('All users get READ & WRITE access. Not recommended. (public-read-write)'),
+        'authenticated-read' => $this->t('Authenticated users get READ access. (authenticated-read)'),
+        'bucket-owner-read' => $this->t('Bucket owner gets READ access. (bucket-owner-read)'),
+        'bucket-owner-full-control' => $this->t('Bucket owner has full control. (bucket-owner-full-control)'),
+      ],
+      '#default_value' => $this->getSetting('s3_acl'),
+    ];
+
+    return $element;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function settingsSummary() {
+    $summary = [];
+    $summary[] = $this->t('ACL: @acl', array('@acl' => $this->getSetting('s3_acl')));
+    return $summary;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
+    $element = parent::formElement($items, $delta, $element, $form, $form_state);
+    $element['#type'] = 'flysystem_s3_cors_managed_file';
+    $element['#acl'] = $this->getSetting('s3_acl');
+
+    $element_info = $this->elementInfo->getInfo('flysystem_s3_cors_managed_file');
+    //$element['#process'] = array_merge($element_info['#process'], array(array(get_called_class(), 'process')));
+dpm($element);
+    return $element;
+  }
+}
