Authenticate Uploads to Amazon S3 in SlimPHP Using Twilio Verify
In this tutorial, you'll learn how to authenticate uploads to an Amazon S3 bucket in a PHP application using Twilio Verify API. The app will verify users by sending them a one-time password via SMS before allowing them to upload images to the S3 bucket. Here are the steps that we'll follow: 1. Set up your development environment. 2. Create an Amazon S3 bucket and configure it for public access. 3. Install Twilio Verify API SDK and set up a new service. 4. Configure the app to use Twilio Verify API and Amazon S3 services. 5. Implement the user authentication flow in the app. 6. Test the app. By the end of this tutorial, you'll have built an application that can verify users before letting them upload images to an S3 bucket. In doing that, you'll have implemented a minimalist authorization layer around the bucket. Let's get started! 1. Set up your development environment To set up your development environment, follow these steps: a. Install PHP 7.4 or later on your local machine. b. Install Composer on your local machine. c. Create a new directory for the app and navigate to it in your terminal. d. Run `composer require slim/slim:"^3.0"` to install Slim Framework version 3. e. Run `composer require aws/aws-sdk-php:^3.0` to install AWS SDK for PHP version 3. f. Run `composer require twilio/twilio-php:^6.0` to install Twilio Verify API SDK version 6. g. Create a new file named public/index.php and open it in your IDE or text editor. h. Add the following code to the file, which will set up the basic structure of the app. ```php <?php require 'vendor/autoload.php'; $app = new \Slim\App(); // Define routes here... $app->run(); ``` 2. Create an Amazon S3 bucket and configure it for public access To create a new S3 bucket, follow these steps: a. Sign in to the AWS Management Console and open the Amazon S3 console at https://console.aws.amazon.com/s3/. b. Choose Create bucket. c. In Bucket name, enter a DNS-compliant name for your new bucket, and then choose Next. d. On the Set properties page, you can change the following settings: - Versioning: Enable versioning to keep multiple versions of an object in the same bucket. - Logging: Enable logging to record S3 API requests made to this bucket. e. Choose Next until the final page, and then choose Create bucket. f. To configure your new bucket for public access, follow these steps: 1. Open the Amazon S3 console at https://console.aws.amazon.com/s3/. 2. Choose the name of the bucket that you want to make publicly accessible. 3. Choose Permissions. 4. Under Bucket Policy, choose Edit. 5. In Policy Document, enter the following policy: ```json { "Version": "2012-10-17", "Statement": [ { "Sid": "PublicReadGetObject", "Effect": "Allow", "Principal": "*", "Action": "s3:GetObject", "Resource": "arn:aws:s3:::your-bucket-name/*" } ] } ``` 6. Replace your-bucket-name with the name of your bucket, and then choose Save changes. 3. Install Twilio Verify API SDK and set up a new service To install Twilio Verify API SDK and set up a new service, follow these steps: a. Sign in to the Twilio Console at https://www.twilio.com/console. b. Navigate to the Verify section by clicking on the "Products & Services" dropdown menu in the left-hand navigation bar and selecting "Verify." c. Click on the "Get Started" button, which will take you to a page where you can create a new service. d. Enter a name for your service (e.g., "ImageUpload"), select "SMS" as the channel, and then click on the "Create Service" button. e. Once the service is created, Twilio will provide you with two important pieces of information: the Account SID and the Auth Token. Save these values somewhere safe, as you'll need them later in the tutorial. 4. Configure the app to use Twilio Verify API and Amazon S3 services To configure your app to use Twilio Verify API and Amazon S3 services, follow these steps: a. Open the .env file in your IDE or text editor and add the following lines of code, replacing the placeholders with your own values: ```ini TWILIO_ACCOUNT_SID=your-twilio-account-sid TWILIO_AUTH_TOKEN=your-twilio-auth-token AWS_REGION=us-west-2 AWS_BUCKET=your-bucket-name ``` b. Open the config.php file in your IDE or text editor and add the following lines of code: ```php <?php return [ 'allowedFileExtensions' => ['jpg', 'jpeg', 'png'], 'uploadDir' => __DIR__ . '/data/uploads/', ]; ``` c. Open the index.php file in your IDE or text editor and add the following lines of code at the beginning of the file: ```php <?php require 'vendor/autoload.php'; $dotenv = Dotenv\Dotenv::create(__DIR__); $dotenv->load(); $config = require 'config.php'; $app = new \Slim\App([ 'settings' => [ 'displayErrorDetails' => true, ], ]); ``` d. Add the following lines of code at the end of the file to set up Twilio Verify API and Amazon S3 services: ```php $app->get('/', function (Request $request, Response $response) { return $this->get('view')->render($response, 'index.html.twig'); }); $app->map(['GET', 'POST'], '/send-code', function (Request $request, Response $response, array $args) { // Handle sending the verification code here... }); $app->map(['GET', 'POST'], '/verify', function (Request $request, Response $response, array $args) { // Handle verifying the user's identity here... }); $app->map(['GET', 'POST'], '/upload', function (Request $request, Response $response, array $args) { // Handle uploading the image to the S3 bucket here... }); $app->get('/success', function (Request $request, Response $response, array $args) { return $this->get('view')->render($response, 'success.html.twig'); }); ``` 5. Implement the user authentication flow in the app To implement the user authentication flow in your app, follow these steps: a. Create a new file named resources/templates/index.html.twig and open it in your IDE or text editor. Then, in that file, paste the following code. ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Enter Your Email Address</title> <link href="/css/styles.css" rel="stylesheet"> </head> <body> <div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8"> <div class="max-w-md w-full space-y-8"> <div class="mt-8 space-y-6"> <h1 class="title"> Please Enter Your Email Address </h1> </div> <form method="POST"> <div class="rounded-md shadow-sm"> <div class="mb-4"> <label for="email" class="label mb-2 block">Email Address</label> <input type="email" id="email" name="email" {% if error %}class="input-field-with-error"{% else %}class="input-field"{% endif %} placeholder="[email protected]"> {% if error %}<p class="error text-red-700">{{ error }}</p>{% endif %} </div> <button type="submit" class="btn"> Submit Email Address </button> </div> </form> </div> </div> </body> </html> ``` Similar to the default route's template, this code contains a form with two elements: A text field, named email, where the user can enter their email address. A button to submit the form. It also uses the default stylesheet that you downloaded earlier. Next, it's time to define the route. To do that, after the default route's definition in public/index.php, paste the following code. ```php $app->map( ['GET', 'POST'], '/send-code', function (Request $request, Response $response, array $args) { $template = 'send_code.html.twig'; $view = $this->get('view'); if ($request->getMethod() === 'POST') { $email = $request->getParsedBody()['email']; if (! filter_var($email, FILTER_VALIDATE_EMAIL)) { return $view->render( $response, $template, ['error' => 'Please enter a valid email address.'] ); } // TODO: Send the verification code to the user via SMS using Twilio Verify API... } return $view->render($response, $template); }); ``` As before, $app->map() is called to define the route with the path /send-code, which can accept both GET and POST requests. The route's handler starts by defining the template to render and retrieving the view service from the DI container. Then, it checks if a POST request was made. If so, it retrieves the email address submitted in the request. If the email address is not valid (i.e., it doesn't match the pattern defined by FILTER_VALIDATE_EMAIL), then it renders the route's template, passing in an error message to display. If the email address was valid, then you need to send a verification code to the user via SMS using Twilio Verify API. To do that, follow these steps: a. Retrieve the Twilio client from the DI container by calling $this->get('twilioClient'). b. Use the Twilio client's verify service (created earlier) to send a verification code to the user via SMS. The code should look something like this: ```php $verification = $twilioClient ->verify ->v2 ->services($_ENV['VERIFY_SERVICE_SID']) ->verifications ->create($email, ["channel" => "sms"]); ``` c. Once the verification code has been sent successfully, redirect the user to the /verify route by calling $response->withHeader('Location', '/verify')->withStatus(302). If a GET request was made, then the template is rendered, ready for the user to enter their email address. Next, create another file named resources/templates/send_code.html.twig and open it in your IDE or text editor. Then, in that file, paste the following code. ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Verify Your Account</title> <link href="/css/styles.css" rel="stylesheet"> </head> <body> <div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8"> <div class="max-w-md w-full space-y-8"> <div class="mt-8 space-y-6"> <h1 class="title"> Please verify your account {{ username }} </h1> </div> <form method="POST"> <div class="rounded-md shadow-sm"> <div class="mb-4"> <label for="verification_code" class="label mb-2 block">Enter the code sent to your phone</label> <input type="password" id="verification_code" name="verification_code" {% if error %}class="input-field-with-error"{% else %}class="input-field"{% endif %} placeholder="verification code"> {% if error %}<p class="error text-red-700">{{ error }}</p>{% endif %} </div> <button type="submit" class="btn"> Submit Verification Code </button> </div> </form> </div> </div> </body> </html> Similar to the default route's template, this code contains a form with two elements: A password field, named verification_code, where the user can enter the verification code that they received via SMS from Twilio after submitting the previous form. A button to submit the form. It also uses the default stylesheet that you downloaded earlier. Next, it's time to define the route. To do that, after the default route's definition in public/index.php, paste the following code. ```php $app->map( ['GET', 'POST'], '/verify', function (Request $request, Response $response, array $args) { $username = $_SESSION['username']; $phoneNumber = $this->get('known_participants')[$username]; $template = 'verify.html.twig'; $view = $this->get('view'); if ($request->getMethod() === 'POST') { $verificationCode = $request->getParsedBody()['verification_code']; if ($verificationCode === '') { return $view->render( $response, $template, ['error' => 'Please enter the verification code.'] ); } $twilioClient = $this->get('twilioClient'); $verification = $twilioClient ->verify ->v2 ->services($_ENV['VERIFY_SERVICE_SID']) ->verificationChecks ->create($verificationCode, ["to" => $phoneNumber]); return ($verification->status === 'approved') ? $response ->withHeader('Location', '/upload') ->withStatus(302) : $view->render( $response, $template, ['error' => 'Invalid verification code. Please try again.'] ); } return $view->render($response, $template); }); ``` As before, $app->map() is called to define the route with the path /verify, which can accept both GET and POST requests. The route's handler starts by retrieving the user's email address, username from the session, and uses it to retrieve the user's phone number from the known participant's database. After that, it sets the template to render and retrieves the view service from the DI container. Then, it checks if a POST request was made. If so, it retrieves the verification code submitted in the request. If the code is empty, it renders the route's template, passing in an error message to display. If the code was not empty, it attempts to validate it; again, using Twilio's Verify API, storing the response, a VerificationInstance object, in a new variable, $verification. From this object, the app can determine if the code validated successfully, by checking the value of the status property. If it is set to approved then validation succeeded, so the user is redirected to the upload route. If validation failed, the user is shown the original form, along with an error message stating that the validation code was invalid. status can have one of three values: pending, approved, and canceled. The route's handler ends by implicitly checking if a GET request was made. If so, the user is shown the form, where they can submit the verification code. Create the route to upload the image to the S3 bucket As with the validation route, two things need to be done to create the upload route: Create the route template. Define the route. As before, start by creating the template file, resources/templates/upload.html.twig and open it in your IDE or text editor. Then, in that file, paste the following code. ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Upload Your Image</title> <link href="/css/styles.css" rel="stylesheet"> </head> <body> <div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8"> <div class="max-w-md w-full space-y-8"> <div class="mt-8 space-y-6"> <h1 class="title"> Please upload your image </h1> </div> <form method="POST" enctype="multipart/form-data"> <div class="rounded-md shadow-sm"> <div class="mb-4"> <label for="image" class="label mb-2 block">Image</label> <input type="file" id="image" name="image" accept="image/jpeg, image/png"> </div> <button type="submit" class="btn"> Upload Image </button> </div> </form> </div> </div> </body> </html> ``` Similar to the default route's template, this code contains a form with two elements: A file input field, named image, where the user can select an image file to upload. A button to submit the form. It also uses the default stylesheet that you downloaded earlier. Next, it's time to define the route. To do that, after the default route's definition in public/index.php, paste the following code. ```php $app->map( ['GET', 'POST'], '/upload', function (Request $request, Response $response, array $args) { $template = 'upload.html.twig'; $view = $this->get('view'); if ($request->getMethod() === 'POST') { // TODO: Handle uploading the image to the S3 bucket here... } return $view->render($response, $template); }); ``` As before, $app->map() is called to define the route with the path /upload, which can accept both GET and POST requests. The route's handler starts by defining the template to render and retrieving the view service from the DI container. Then, it checks if a POST request was made. If so, it retrieves the image file submitted in the request. You can access this file using $request->getUploadedFiles()['image']. Next, you need to upload the image file to the S3 bucket by following these steps: a. Retrieve the Amazon S3 client from the DI container by calling $this->get('s3Client'). b. Use the Amazon S3 client's putObject method to upload the image file to the S3 bucket. The code should look something like this: ```php $file = $request->getUploadedFiles()['image']; if ($file->getError() === UPLOAD_ERR_OK) { $s3Client = $this->get('s3Client'); $result = $s3Client->putObject([ 'Bucket' => $_ENV['AWS_BUCKET'], 'Key' => $file->getClientFilename(), 'Body' => fopen($file->getRealPath(), 'r'), ]); } ``` c. Once the image file has been uploaded successfully, redirect the user to the /success route by calling $response->withHeader('Location', '/success')->withStatus(302). If a GET request was made, then the template is rendered, ready for the user to select an image file to upload. 6. Test the app To test your app, follow these steps: a. Start the PHP development server by running `php -S localhost:8000` in your terminal while you're in the root directory of your app. b. Open a web browser and navigate to http://localhost:8000/. c. Enter your email address in the form and click on the "Submit Email Address" button. d. Check your phone for a text message containing the verification code. e. Enter the verification code in the form and click on the "Submit Verification Code" button. f. If validation was successful, you should be redirected to the image upload page. Select an image file to upload and click on the "Upload Image" button. g. Check your Amazon S3 bucket for the uploaded image file. h. If everything worked as expected, congratulations! You've successfully built an app that can verify users before letting them upload images to an S3 bucket using Twilio Verify API and Amazon S3 services.
Company
Twilio
Date published
Nov. 23, 2021
Author(s)
Matthew Setter
Word count
4789
Hacker News points
None found.
Language
English