Add native S3 Presigned POST URL support to S3Client
- Dominant language
- Java
- Stars
- 2.6k
- Forks
- 1k
- Avg merge
- 2d 9h
- Merged PRs (30d)
- 51
Description
### Describe the feature
## Description
The AWS SDK for Java V2 should provide native support for generating presigned POST URLs with policy documents directly in the `S3Client` or `S3Presigner`, eliminating the need for workarounds or external implementations.
## Background
This feature has been requested since 2019 in issue #1493, but remains unimplemented. Currently, developers must resort to workarounds such as:
- Calling AWS Lambda functions written in other languages (JavaScript/Python)
- Implementing custom signing logic
- Using third-party libraries
The V1 SDK also lacked this feature (aws/aws-sdk-java#834), making this a long-standing gap in the Java SDK ecosystem.
## Justification
**1. Feature Parity with Other SDKs**
- **Python (boto3)**: Provides `s3.generate_presigned_post()` with policy conditions
- **JavaScript**: Supports `createPresignedPost()` with field policies
- **Go**: Includes `PresignPostObject()` functionality
**2. Security & Best Practices**
Presigned POST URLs with policy documents enable:
- Fine-grained access control without exposing AWS credentials
- Content-type validation to prevent malicious uploads
- File size limits to prevent abuse
- Key prefix restrictions to enforce folder structure
## Related Issues
- #1493 - Original feature request (2019)
- #203 - Related presigning discussion
- aws/aws-sdk-java#834 - V1 SDK request
## I Would Like to Implement This
I am currently implementing S3 POST support in S3 Ninja (an S3-compatible test server) and would be happy to contribute this feature to the SDK. Please assign this issue to me if accepted.
Having this feature would greatly benefit:
- Developers building modern web applications with direct browser uploads
- Teams testing S3 integrations locally with tools like S3 Ninja, LocalStack, or MinIO
- Organizations requiring fine-grained upload control without credential exposure
### Use Case
Browser-based direct uploads to S3 are a standard pattern in modern web applications, requiring:
- Pre-flight authorization checks on the backend
- Secure, time-limited upload URLs
- Policy enforcement (file size, content type, key prefix restrictions)
## Use Case Example
### Backend (Java)
```java
@PostMapping("/upload/presign")
public UploadCredentials getUploadUrl(@RequestBody UploadRequest request) {
// Validate user permissions
if (!authService.canUpload(request.getUserId())) {
throw new ForbiddenException();
}
// Generate presigned POST
PresignedPostResponse postResponse = s3Presigner.presignPost(req -> req
.bucket("user-uploads")
.key("users/" + request.getUserId() + "/${filename}")
.expiration(Instant.now().plus(Duration.ofMinutes(15)))
.conditions(conditions -> conditions
.contentLengthRange(1024, 5_242_880) // 1KB-5MB
.startsWith("key", "users/" + request.getUserId())
.eq("Content-Type", request.getContentType())));
return new UploadCredentials(
postResponse.url(),
postResponse.formFields()
);
}
```
### Frontend (JavaScript)
```javascript
// Get presigned POST from backend
const { url, fields } = await fetch('/upload/presign', {
method: 'POST',
body: JSON.stringify({ userId: '12345', contentType: 'image/jpeg' })
}).then(r => r.json());
// Upload directly to S3
const formData = new FormData();
Object.entries(fields).forEach(([key, value]) => {
formData.append(key, value);
});
formData.append('file', fileInput.files[0]);
await fetch(url, { method: 'POST', body: formData });
```
### Proposed Solution
## Proposed API
### Option 1: Extend `S3Presigner`
```java
S3Presigner presigner = S3Presigner.create();
PresignedPostRequest postRequest = PresignedPostRequest.builder()
.bucket("my-bucket")
.key("uploads/${filename}")
.expiration(Instant.now().plus(Duration.ofMinutes(15)))
.conditions(PolicyConditions.builder()
.contentLengthRange(1024, 10_485_760) // 1KB to 10MB
.startsWith("key", "uploads/")
.eq("Content-Type", "image/jpeg")
.eq("x-amz-meta-user-id", "12345")
.build())
.build();
PresignedPostResponse response = presigner.presignPost(postRequest);
// Returns:
// - URL: https://my-bucket.s3.amazonaws.com/
// - Form fields: Map containing policy, signature, etc.
```
### Option 2: Add to `S3Utilities`
```java
S3Utilities utilities = S3Utilities.builder()
.region(Region.US_EAST_1)
.credentialsProvider(credentialsProvider)
.build();
PostSigningResult result = utilities.signPostRequest(request -> request
.bucket("my-bucket")
.key("uploads/${filename}")
.expirationTime(Instant.now().plus(Duration.ofMinutes(15)))
.addCondition("content-length-range", 1024, 10_485_760)
.addCondition("starts-with", "$key", "uploads/")
.addCondition("eq", "$Content-Type", "image/jpeg"));
String url = result.url();
Map formFields = result.formFields();
```
## Implementation Considerations
**Policy Document Structure:**
```json
{
"expiration": "2024-12-01T12:00:00.000Z",
"conditions": [
{"bucket": "my-bucket"},
["starts-with", "$key", "uploads/"],
{"Content-Type": "image/jpeg"},
["content-length-range", 1024, 10485760],
{"x-amz-algorithm": "AWS4-HMAC-SHA256"},
{"x-amz-credential": "AKIAIOSFODNN7EXAMPLE/20241121/us-east-1/s3/aws4_request"},
{"x-amz-date": "20241121T000000Z"}
]
}
```
**Form Fields to Return:**
- `key`: Object key (can include `${filename}` placeholder)
- `policy`: Base64-encoded policy document
- `x-amz-algorithm`: Signing algorithm
- `x-amz-credential`: Credential scope
- `x-amz-date`: Request date
- `x-amz-signature`: Calculated signature
- Any additional fields from policy conditions
**Signature Calculation:**
Should follow AWS Signature Version 4 for POST requests as documented in the [S3 API Reference](https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectPOST.html).
### Other Information
_No response_
### Acknowledgements
- [x] I may be able to implement this feature request
- [ ] This feature might incur a breaking change
### AWS Java SDK version used
2
### JDK version used
24
### Operating System and version
Windows 11
Contributor guide
Assessment
This issue has not been assessed yet.