Skip to content

Add native S3 Presigned POST URL support to S3Client #6577

@Romeo-mz

Description

@Romeo-mz

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

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)

@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)

// 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

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<String, String> containing policy, signature, etc.

Option 2: Add to S3Utilities

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<String, String> formFields = result.formFields();

Implementation Considerations

Policy Document Structure:

{
  "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.

Other Information

No response

Acknowledgements

  • 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

Metadata

Metadata

Assignees

Labels

feature-requestA feature should be added or improved.needs-reviewThis issue or PR needs review from the team.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions