0
# Presigned Operations
1
2
This document covers presigned URL generation and POST policy creation for secure, time-limited access to MinIO/S3 objects without exposing credentials to clients.
3
4
## Presigned URLs
5
6
Presigned URLs allow clients to access objects directly without requiring AWS credentials. These URLs are signed with your credentials and have an expiration time.
7
8
### Generic Presigned URL
9
10
```javascript { .api }
11
const url = await client.presignedUrl(method, bucketName, objectName, expires?, reqParams?, requestDate?)
12
13
// Parameters
14
method: string // HTTP method ('GET', 'PUT', 'POST', 'DELETE')
15
bucketName: string // Bucket name
16
objectName: string // Object name/key
17
expires?: number // Expiry time in seconds (default: 604800 = 7 days)
18
reqParams?: PreSignRequestParams // Request parameters and headers
19
requestDate?: Date // Request date (default: current time)
20
21
// Returns: Promise<string> - Presigned URL
22
```
23
24
#### PreSignRequestParams Interface
25
26
```typescript { .api }
27
interface PreSignRequestParams {
28
// Query parameters to include in URL
29
[key: string]: string | undefined
30
31
// Common parameters
32
'response-content-type'?: string // Override Content-Type in response
33
'response-content-language'?: string // Override Content-Language in response
34
'response-expires'?: string // Override Expires in response
35
'response-cache-control'?: string // Override Cache-Control in response
36
'response-content-disposition'?: string // Override Content-Disposition in response
37
'response-content-encoding'?: string // Override Content-Encoding in response
38
39
// Version selection
40
'versionId'?: string // Specific version ID
41
}
42
```
43
44
### Presigned GET URL
45
46
```javascript { .api }
47
const url = await client.presignedGetObject(bucketName, objectName, expires?, respHeaders?, requestDate?)
48
49
// Parameters
50
bucketName: string // Bucket name
51
objectName: string // Object name/key
52
expires?: number // Expiry time in seconds (max: 604800)
53
respHeaders?: PreSignRequestParams // Response headers to override
54
requestDate?: Date // Request date (default: current time)
55
56
// Returns: Promise<string> - Presigned GET URL
57
```
58
59
### Presigned PUT URL
60
61
```javascript { .api }
62
const url = await client.presignedPutObject(bucketName, objectName, expires?)
63
64
// Parameters
65
bucketName: string // Bucket name
66
objectName: string // Object name/key
67
expires?: number // Expiry time in seconds (max: 604800)
68
69
// Returns: Promise<string> - Presigned PUT URL
70
```
71
72
### Presigned URL Examples
73
74
```javascript { .api }
75
// Generate presigned GET URL (1 hour expiry)
76
const downloadUrl = await client.presignedGetObject('my-bucket', 'photo.jpg', 3600)
77
console.log('Download URL:', downloadUrl)
78
// Client can now download directly: fetch(downloadUrl)
79
80
// Generate presigned PUT URL (30 minutes expiry)
81
const uploadUrl = await client.presignedPutObject('my-bucket', 'upload.pdf', 1800)
82
console.log('Upload URL:', uploadUrl)
83
// Client can now upload: fetch(uploadUrl, { method: 'PUT', body: file })
84
85
// Presigned GET with custom response headers
86
const customUrl = await client.presignedGetObject('my-bucket', 'document.pdf', 3600, {
87
'response-content-type': 'application/octet-stream',
88
'response-content-disposition': 'attachment; filename="custom-name.pdf"'
89
})
90
// Downloaded file will have custom headers
91
92
// Presigned URL for specific version
93
const versionUrl = await client.presignedGetObject('versioned-bucket', 'file.txt', 3600, {
94
'versionId': 'version-123'
95
})
96
97
// Generic presigned URL for DELETE operation
98
const deleteUrl = await client.presignedUrl('DELETE', 'my-bucket', 'temp-file.txt', 600)
99
// Client can delete: fetch(deleteUrl, { method: 'DELETE' })
100
101
// Presigned URL with custom request date
102
const customDateUrl = await client.presignedUrl(
103
'GET',
104
'my-bucket',
105
'file.txt',
106
3600,
107
{},
108
new Date('2023-12-25T00:00:00Z') // Custom signing date
109
)
110
```
111
112
## POST Policy
113
114
POST policies allow secure, direct browser uploads to S3 with fine-grained control over upload conditions and metadata.
115
116
### PostPolicy Class
117
118
```typescript { .api }
119
import { PostPolicy } from 'minio'
120
121
class PostPolicy {
122
// Properties
123
policy: {
124
conditions: (string | number)[][] // Policy conditions array
125
expiration?: string // Policy expiration timestamp
126
}
127
formData: Record<string, string> // Form data for POST request
128
129
// Methods
130
setExpires(date: Date): void
131
setKey(objectName: string): void
132
setKeyStartsWith(prefix: string): void
133
setBucket(bucketName: string): void
134
setContentType(type: string): void
135
setContentTypeStartsWith(prefix: string): void
136
setContentDisposition(value: string): void
137
setContentLengthRange(min: number, max: number): void
138
setUserMetaData(metaData: ObjectMetaData): void
139
}
140
```
141
142
### Create New POST Policy
143
144
Creates a new PostPolicy instance for defining upload conditions and generating presigned POST policies.
145
146
```javascript { .api }
147
const postPolicy = client.newPostPolicy()
148
149
// Returns: PostPolicy instance
150
```
151
152
#### Examples
153
154
```javascript
155
// Create new post policy
156
const postPolicy = client.newPostPolicy()
157
158
// Set policy conditions
159
postPolicy.setBucket('my-bucket')
160
postPolicy.setKey('uploads/file.jpg')
161
postPolicy.setExpires(new Date(Date.now() + 24 * 60 * 60 * 1000)) // 24 hours
162
postPolicy.setContentType('image/jpeg')
163
postPolicy.setContentLengthRange(1024, 5 * 1024 * 1024) // 1KB to 5MB
164
165
// Generate presigned policy
166
const presignedPostPolicy = await client.presignedPostPolicy(postPolicy)
167
168
// Alternative: Using PostPolicy constructor directly
169
import { PostPolicy } from 'minio'
170
const altPolicy = new PostPolicy()
171
altPolicy.setBucket('my-bucket')
172
// ... set conditions
173
```
174
175
### Generate Presigned POST Policy
176
177
```javascript { .api }
178
const result = await client.presignedPostPolicy(postPolicy)
179
180
// Parameters
181
postPolicy: PostPolicy // POST policy configuration
182
183
// Returns: Promise<PostPolicyResult>
184
```
185
186
#### PostPolicyResult Interface
187
188
```typescript { .api }
189
interface PostPolicyResult {
190
postURL: string // URL to POST to
191
formData: Record<string, string> // Form fields to include in POST
192
}
193
```
194
195
### POST Policy Examples
196
197
```javascript { .api }
198
import { PostPolicy } from 'minio'
199
200
// Basic POST policy for browser uploads
201
const policy = new PostPolicy()
202
203
// Set policy expiration (required)
204
const expires = new Date()
205
expires.setMinutes(expires.getMinutes() + 10) // 10 minutes from now
206
policy.setExpires(expires)
207
208
// Set bucket and object constraints
209
policy.setBucket('upload-bucket')
210
policy.setKey('user-uploads/photo.jpg') // Exact key
211
// OR
212
policy.setKeyStartsWith('user-uploads/') // Key prefix
213
214
// Set content type constraints
215
policy.setContentType('image/jpeg') // Exact type
216
// OR
217
policy.setContentTypeStartsWith('image/') // Type prefix
218
219
// Set file size limits (1KB to 10MB)
220
policy.setContentLengthRange(1024, 10 * 1024 * 1024)
221
222
// Add custom metadata
223
policy.setUserMetaData({
224
'uploaded-by': 'web-client',
225
'upload-session': 'abc123'
226
})
227
228
// Generate presigned POST policy
229
const { postURL, formData } = await client.presignedPostPolicy(policy)
230
231
console.log('POST URL:', postURL)
232
console.log('Form data:', formData)
233
234
// Advanced POST policy with multiple conditions
235
const advancedPolicy = new PostPolicy()
236
237
// Set expiration (1 hour)
238
const expiry = new Date()
239
expiry.setHours(expiry.getHours() + 1)
240
advancedPolicy.setExpires(expiry)
241
242
// Multiple conditions
243
advancedPolicy.setBucket('secure-uploads')
244
advancedPolicy.setKeyStartsWith('documents/') // Only allow documents/ prefix
245
advancedPolicy.setContentTypeStartsWith('application/') // Only application/* types
246
advancedPolicy.setContentLengthRange(100, 50 * 1024 * 1024) // 100B to 50MB
247
advancedPolicy.setContentDisposition('attachment') // Force download
248
249
// Custom metadata for tracking
250
advancedPolicy.setUserMetaData({
251
'department': 'legal',
252
'classification': 'internal',
253
'retention-years': '7'
254
})
255
256
const advancedResult = await client.presignedPostPolicy(advancedPolicy)
257
```
258
259
### Browser POST Upload Example
260
261
```html
262
<!-- HTML form for browser upload using POST policy -->
263
<!DOCTYPE html>
264
<html>
265
<head>
266
<title>Direct S3 Upload</title>
267
</head>
268
<body>
269
<form id="upload-form" method="POST" enctype="multipart/form-data">
270
<!-- Form fields from formData -->
271
<input type="hidden" name="key" value="">
272
<input type="hidden" name="policy" value="">
273
<input type="hidden" name="x-amz-algorithm" value="">
274
<input type="hidden" name="x-amz-credential" value="">
275
<input type="hidden" name="x-amz-date" value="">
276
<input type="hidden" name="x-amz-signature" value="">
277
278
<!-- File input -->
279
<input type="file" name="file" required>
280
<button type="submit">Upload</button>
281
</form>
282
283
<script>
284
// JavaScript to populate form and handle upload
285
async function setupUpload() {
286
// Get presigned POST policy from your backend
287
const response = await fetch('/api/presigned-post-policy', {
288
method: 'POST',
289
headers: { 'Content-Type': 'application/json' },
290
body: JSON.stringify({
291
bucket: 'upload-bucket',
292
keyPrefix: 'user-uploads/',
293
maxSize: 10 * 1024 * 1024
294
})
295
})
296
297
const { postURL, formData } = await response.json()
298
299
// Set form action to POST URL
300
const form = document.getElementById('upload-form')
301
form.action = postURL
302
303
// Populate hidden fields
304
Object.entries(formData).forEach(([name, value]) => {
305
const input = form.querySelector(`input[name="${name}"]`)
306
if (input) input.value = value
307
})
308
309
// Handle form submission
310
form.addEventListener('submit', async (e) => {
311
e.preventDefault()
312
313
const formData = new FormData(form)
314
const file = formData.get('file')
315
316
if (!file) {
317
alert('Please select a file')
318
return
319
}
320
321
try {
322
const uploadResponse = await fetch(postURL, {
323
method: 'POST',
324
body: formData
325
})
326
327
if (uploadResponse.ok) {
328
alert('Upload successful!')
329
} else {
330
alert('Upload failed: ' + uploadResponse.statusText)
331
}
332
} catch (error) {
333
alert('Upload error: ' + error.message)
334
}
335
})
336
}
337
338
setupUpload()
339
</script>
340
</body>
341
</html>
342
```
343
344
## Advanced Presigned Operations
345
346
### Presigned URL with Custom Headers
347
348
```javascript { .api }
349
// Presigned PUT URL that enforces specific headers
350
const putUrl = await client.presignedUrl('PUT', 'my-bucket', 'strict-upload.pdf', 3600, {
351
// These parameters will be included in the signature
352
'x-amz-server-side-encryption': 'AES256',
353
'x-amz-meta-uploader': 'api-service'
354
})
355
356
// Client must include these exact headers when making the PUT request
357
// fetch(putUrl, {
358
// method: 'PUT',
359
// body: file,
360
// headers: {
361
// 'x-amz-server-side-encryption': 'AES256',
362
// 'x-amz-meta-uploader': 'api-service'
363
// }
364
// })
365
```
366
367
### Batch Presigned URL Generation
368
369
```javascript { .api }
370
// Generate multiple presigned URLs efficiently
371
async function generateBatchUrls(bucketName, objectNames, expires = 3600) {
372
const urls = await Promise.all(
373
objectNames.map(objectName =>
374
client.presignedGetObject(bucketName, objectName, expires)
375
)
376
)
377
378
return objectNames.reduce((acc, objectName, index) => {
379
acc[objectName] = urls[index]
380
return acc
381
}, {})
382
}
383
384
const objectNames = ['file1.pdf', 'file2.jpg', 'file3.txt']
385
const urlMap = await generateBatchUrls('my-bucket', objectNames)
386
387
// Usage
388
Object.entries(urlMap).forEach(([objectName, url]) => {
389
console.log(`${objectName}: ${url}`)
390
})
391
```
392
393
### Time-Limited Batch Operations
394
395
```javascript { .api }
396
// Create time-limited upload slots for multiple files
397
async function createUploadSlots(bucketName, fileNames, validMinutes = 30) {
398
const policy = new PostPolicy()
399
400
// Set expiration
401
const expires = new Date()
402
expires.setMinutes(expires.getMinutes() + validMinutes)
403
policy.setExpires(expires)
404
405
// Configure bucket and key prefix
406
policy.setBucket(bucketName)
407
policy.setKeyStartsWith('batch-uploads/')
408
409
// Allow various content types
410
policy.setContentLengthRange(1, 100 * 1024 * 1024) // 1B to 100MB
411
412
// Generate URLs for each file
413
const uploadSlots = []
414
415
for (const fileName of fileNames) {
416
// Create separate policy for each file for specific key
417
const filePolicy = new PostPolicy()
418
filePolicy.setExpires(expires)
419
filePolicy.setBucket(bucketName)
420
filePolicy.setKey(`batch-uploads/${Date.now()}-${fileName}`)
421
filePolicy.setContentLengthRange(1, 100 * 1024 * 1024)
422
423
const result = await client.presignedPostPolicy(filePolicy)
424
uploadSlots.push({
425
fileName,
426
...result
427
})
428
}
429
430
return uploadSlots
431
}
432
433
const files = ['document1.pdf', 'image1.jpg', 'data1.csv']
434
const uploadSlots = await createUploadSlots('batch-bucket', files, 15)
435
436
uploadSlots.forEach(slot => {
437
console.log(`Upload slot for ${slot.fileName}:`)
438
console.log(` URL: ${slot.postURL}`)
439
console.log(` Form data:`, slot.formData)
440
})
441
```
442
443
## Security Considerations
444
445
### URL Expiration Limits
446
447
```javascript { .api }
448
import { PRESIGN_EXPIRY_DAYS_MAX } from 'minio'
449
450
console.log('Maximum expiry:', PRESIGN_EXPIRY_DAYS_MAX, 'seconds') // 604800 (7 days)
451
452
// This will throw an error
453
try {
454
await client.presignedGetObject('bucket', 'object', PRESIGN_EXPIRY_DAYS_MAX + 1)
455
} catch (error) {
456
console.error('Expiry too long:', error.message)
457
}
458
459
// Safe expiry times
460
const oneHour = 3600
461
const oneDay = 24 * 3600
462
const oneWeek = 7 * 24 * 3600 // Maximum allowed
463
464
const url = await client.presignedGetObject('bucket', 'object', oneWeek)
465
```
466
467
### POST Policy Validation
468
469
```javascript { .api }
470
// Strict POST policy with multiple security constraints
471
const securePolicy = new PostPolicy()
472
473
// Short expiration for security
474
const shortExpiry = new Date()
475
shortExpiry.setMinutes(shortExpiry.getMinutes() + 5) // 5 minutes only
476
securePolicy.setExpires(shortExpiry)
477
478
// Restrict to specific bucket and path
479
securePolicy.setBucket('secure-uploads')
480
securePolicy.setKeyStartsWith('verified-users/')
481
482
// Strict content type validation
483
securePolicy.setContentType('image/jpeg') // Only JPEG images
484
485
// Strict size limits
486
securePolicy.setContentLengthRange(1024, 2 * 1024 * 1024) // 1KB to 2MB
487
488
// Add security metadata
489
securePolicy.setUserMetaData({
490
'security-scan': 'pending',
491
'upload-source': 'verified-client'
492
})
493
494
const secureResult = await client.presignedPostPolicy(securePolicy)
495
```
496
497
## Error Handling
498
499
```javascript { .api }
500
import {
501
S3Error,
502
ExpiresParamError,
503
InvalidArgumentError
504
} from 'minio'
505
506
try {
507
// Invalid expiry (too long)
508
await client.presignedGetObject('bucket', 'object', 10 * 24 * 3600) // 10 days
509
} catch (error) {
510
if (error instanceof ExpiresParamError) {
511
console.error('Invalid expiry time:', error.message)
512
}
513
}
514
515
try {
516
// Invalid bucket name
517
await client.presignedPutObject('invalid..bucket', 'object.txt')
518
} catch (error) {
519
if (error instanceof InvalidArgumentError) {
520
console.error('Invalid bucket name:', error.message)
521
}
522
}
523
524
// Handle POST policy errors
525
try {
526
const policy = new PostPolicy()
527
// Missing required expiration
528
await client.presignedPostPolicy(policy)
529
} catch (error) {
530
console.error('Policy error:', error.message)
531
}
532
```
533
534
## Best Practices
535
536
### 1. Security
537
- Use shortest reasonable expiration times
538
- Validate client requests match presigned parameters
539
- Monitor presigned URL usage and abuse
540
- Consider IP restrictions for sensitive operations
541
542
### 2. Performance
543
- Cache presigned URLs when appropriate
544
- Generate URLs in batches for efficiency
545
- Use POST policies for browser uploads
546
- Consider CDN integration for download URLs
547
548
### 3. User Experience
549
- Provide clear upload progress feedback
550
- Handle upload failures gracefully
551
- Validate files on client side before upload
552
- Use appropriate content-disposition headers
553
554
### 4. Monitoring
555
- Log presigned URL generation and usage
556
- Monitor for unusual access patterns
557
- Track upload success/failure rates
558
- Set up alerts for policy violations
559
560
### 5. Integration
561
- Separate presigned URL generation from client logic
562
- Use backend services to generate URLs securely
563
- Implement proper CORS for browser uploads
564
- Handle different file types appropriately
565
566
---
567
568
**Next:** [Notifications](./notifications.md) - Learn about event notification system and polling