S3 pre-signed URL with FirebaseAuth in Flutter
System Design Principle
The design decision is made based on below:
- Security: Protect user’s data, also protect my infrastructures
- Cost: As cheap as possible during the startup phase.
Use case
Attaching an image in a mobile app is a very common use case, for example, the user profile image, the item’s image, or a meme image. I will focus on the writing (or put, or upload) case. For reading, we also can use pre-signed url, or make the s3 bucket as public, or other options. I may cover it in other articles.
AWS S3 might be the first choice as the image storage. It supports the pre-signed URL, this URL can grant the temporary permission to access (read/write) the file temporarily without exposing the aws access key pair to clients.
What we have (or limitation)
- AWS S3 : support pre-signed url.
- Firebase AuthN : Manage the user authentication, other authN solution also works as long as it supports JWT.
- Backend service: (AWS ApiGateway + Lambda or others, doesn’t matter in this article)
- JWT: the backend service can use it to verify the user’s authentication with Firebase AuthN
- Flutter: No official S3 SDK for Flutter(or Dart) AFAIK (searched),
amplify_storage_s3
is the only (right?) official SDK from AWS however it works for Amplify (wrapper of a few AWS services including S3) based on S3, not S3’s SDK. (pay extra to Amplify service).
Remember the limitation that Flutter doesn’t have the official S3 SDK.
Options
the user has signed in the app, and want to upload an image.
We may have below options:
- The backend service provides an upload api. The app can call the api to upload image, then the service talks to S3 for saving the image.
- The backend service provides an api to return the pre-signed s3 url to the app, the app uses the url to read/write image from/to s3.
- Copy the AWS IAM User access key pair into the app (using env to import or hardcode), then generate the pre-signed url on the client side.
- Call AWS IAM role to get the temporary access key without talking to the backend service. Generate the pre-signed url on the client side, then use the url to
Put
object.
Making the decision
First, option 3 is not recommended and should be excluded. Building the IAM User (long term credential) into the app or commit it into code repository (even private repository) may expose it to the public. It’s not safe totally. The hacker can use the credentials to upload the unlimited image even without signing up to the mobile app.
Option 1 Upload Image APi
Pros
- High security level.
- Can be asynchronous.
- Error handling in service, good customer exp.
- Can identify customer’s bad behavior quickly or build rule.
Cons
- Cost is high
- Ingress bandwidth cost
- Request cost
Option 2 Pre-signed Url API
Pros
- Medium security level.
- Can adjust the Url config per customer and control the s3 path
- the TTL
- the image name, s3 path
Cons
- Cost is high.
- Request cost
- Error handing in the app.
Option 4 Generate Pre-signed Url in the app
Pros
- Low security level.
- Cost is low.
- call AWS STS assume role api is free
Cons
- Error handing in the app.
- the short term credential is exposed to client.
- client control the image name, s3 path.
Decision
I choose option 4 as the low cost and the security can meet the industry standard.
Worst cases
- the pre-signed url is exposed to a hacker: the hacker can use this url to upload unlimited images to the same s3 path (pre-defined in the url) until the url gets expiration.
- the short term credential is exposed to a hacker: the hacker can use the credential to generate pre-signed url with random s3 path and upload them, until the short term credential gets expiration.
Mitigation
- Reduce the TTL on the pre-signed url.
- Reduce the TTL on the short term credential.
- Grant the minimal permission to the IAM Role (with the short term credential), like only can write object to specific s3 bucket or path.
- Associate the user’s identity to the short term credential, so it’s possible to audit the log in AWS CloudTrail (find who is doing the bad behavior). Check the full code in gist
Alternative
For option 3 and 4, we also can use S3 Rest api PutObject
instead of the pre-signed url. We have to deal with the little complex http headers (almost same to the pre-signed url).
We prefer to pre-signed url because aws_signature_v4 is available, it’s the official SDK from AWS (by Amplify).
Implementation
Infra
AWS ApiGateway and AWS IAM Assume Role both support the JWT (part of the OpenIdConnect and OAuth) so it requires some works to set up the infra.
- AWS ApiGateway guidance: https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-jwt-authorizer.html
- AWS IAM guidance: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_oidc_manual.html
- the configuration from Firebase AuthN to set up in AWS: https://firebase.google.com/docs/auth/admin/verify-id-tokens#verify_id_tokens_using_a_third-party_jwt_library
Code
main libraries:
code gist:
https://gist.github.com/c3qo/c59e942e48229d6f5e1ce4a2c84a8022
Fun facts
- ChatGPT 3.5 can generate the workable code (not verified on device 😜 ).
- For image storage solution, I migrated to other Iaas after the beta testing, not using S3.
Updates
Created on 2023-10-09