Skip to content

Commit f5f0ec4

Browse files
committed
feat: s3 Signed URLs
1 parent 386b05c commit f5f0ec4

File tree

7 files changed

+239
-64
lines changed

7 files changed

+239
-64
lines changed

src/config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ type StorageConfigType = {
103103
s3ProtocolEnforceRegion: boolean
104104
s3ProtocolAccessKeyId?: string
105105
s3ProtocolAccessKeySecret?: string
106+
s3ProtocolNonCanonicalHostHeader?: string
106107
tracingMode?: string
107108
}
108109

@@ -232,6 +233,9 @@ export function getConfig(options?: { reload?: boolean }): StorageConfigType {
232233
s3ProtocolEnforceRegion: getOptionalConfigFromEnv('S3_PROTOCOL_ENFORCE_REGION') === 'true',
233234
s3ProtocolAccessKeyId: getOptionalConfigFromEnv('S3_PROTOCOL_ACCESS_KEY_ID'),
234235
s3ProtocolAccessKeySecret: getOptionalConfigFromEnv('S3_PROTOCOL_ACCESS_KEY_SECRET'),
236+
s3ProtocolNonCanonicalHostHeader: getOptionalConfigFromEnv(
237+
'S3_PROTOCOL_NON_CANONICAL_HOST_HEADER'
238+
),
235239
// Storage
236240
storageBackendType: getOptionalConfigFromEnv('STORAGE_BACKEND') as StorageBackendType,
237241

src/http/plugins/log-request.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,12 @@ export const logRequest = (options: RequestLoggerOptions) =>
5252
}
5353

5454
const rMeth = req.method
55-
const rUrl = redactQueryParamFromRequest(req, ['token'])
55+
const rUrl = redactQueryParamFromRequest(req, [
56+
'token',
57+
'X-Amz-Credential',
58+
'X-Amz-Signature',
59+
'X-Amz-Security-Token',
60+
])
5661
const uAgent = req.headers['user-agent']
5762
const rId = req.id
5863
const cIP = req.ip
@@ -78,7 +83,12 @@ export const logRequest = (options: RequestLoggerOptions) =>
7883
}
7984

8085
const rMeth = req.method
81-
const rUrl = redactQueryParamFromRequest(req, ['token'])
86+
const rUrl = redactQueryParamFromRequest(req, [
87+
'token',
88+
'X-Amz-Credential',
89+
'X-Amz-Signature',
90+
'X-Amz-Security-Token',
91+
])
8292
const uAgent = req.headers['user-agent']
8393
const rId = req.id
8494
const cIP = req.ip

src/http/plugins/signature-v4.ts

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,25 +18,22 @@ const {
1818
s3ProtocolEnforceRegion,
1919
s3ProtocolAccessKeyId,
2020
s3ProtocolAccessKeySecret,
21+
s3ProtocolNonCanonicalHostHeader,
2122
} = getConfig()
2223

23-
export const signatureV4 = fastifyPlugin(async function (fastify: FastifyInstance) {
24-
fastify.addHook('preHandler', async (request: FastifyRequest) => {
25-
if (typeof request.headers.authorization !== 'string') {
26-
throw ERRORS.AccessDenied('Missing authorization header')
27-
}
24+
type AWSRequest = FastifyRequest<{ Querystring: { 'X-Amz-Credential'?: string } }>
2825

29-
const clientCredentials = SignatureV4.parseAuthorizationHeader(request.headers.authorization)
26+
export const signatureV4 = fastifyPlugin(async function (fastify: FastifyInstance) {
27+
fastify.addHook('preHandler', async (request: AWSRequest) => {
28+
const clientSignature = extractSignature(request)
3029

31-
const sessionToken = request.headers['x-amz-security-token'] as string | undefined
30+
const sessionToken = clientSignature.sessionToken
3231

3332
const {
3433
signature: signatureV4,
3534
claims,
3635
token,
37-
} = await createSignature(request.tenantId, clientCredentials, {
38-
sessionToken: sessionToken,
39-
})
36+
} = await createServerSignature(request.tenantId, clientSignature)
4037

4138
const isVerified = signatureV4.verify({
4239
url: request.url,
@@ -45,9 +42,7 @@ export const signatureV4 = fastifyPlugin(async function (fastify: FastifyInstanc
4542
method: request.method,
4643
query: request.query as Record<string, string>,
4744
prefix: s3ProtocolPrefix,
48-
credentials: clientCredentials.credentials,
49-
signature: clientCredentials.signature,
50-
signedHeaders: clientCredentials.signedHeaders,
45+
clientSignature: clientSignature,
5146
})
5247

5348
if (!isVerified && !sessionToken) {
@@ -94,15 +89,23 @@ export const signatureV4 = fastifyPlugin(async function (fastify: FastifyInstanc
9489
})
9590
})
9691

97-
async function createSignature(
98-
tenantId: string,
99-
clientSignature: ClientSignature,
100-
session?: { sessionToken?: string }
101-
) {
92+
function extractSignature(req: AWSRequest) {
93+
if (typeof req.headers.authorization === 'string') {
94+
return SignatureV4.parseAuthorizationHeader(req.headers)
95+
}
96+
97+
if (typeof req.query['X-Amz-Credential'] === 'string') {
98+
return SignatureV4.parseQuerySignature(req.query)
99+
}
100+
101+
throw ERRORS.AccessDenied('Missing signature')
102+
}
103+
104+
async function createServerSignature(tenantId: string, clientSignature: ClientSignature) {
102105
const awsRegion = storageS3Region
103106
const awsService = 's3'
104107

105-
if (session?.sessionToken) {
108+
if (clientSignature?.sessionToken) {
106109
const tenantAnonKey = isMultitenant ? (await getTenantConfig(tenantId)).anonKey : anonKey
107110

108111
if (!tenantAnonKey) {
@@ -112,6 +115,7 @@ async function createSignature(
112115
const signature = new SignatureV4({
113116
enforceRegion: s3ProtocolEnforceRegion,
114117
allowForwardedHeader: s3ProtocolAllowForwardedHeader,
118+
nonCanonicalForwardedHost: s3ProtocolNonCanonicalHostHeader,
115119
credentials: {
116120
accessKey: tenantId,
117121
secretKey: tenantAnonKey,
@@ -120,7 +124,7 @@ async function createSignature(
120124
},
121125
})
122126

123-
return { signature, claims: undefined, token: session.sessionToken }
127+
return { signature, claims: undefined, token: clientSignature.sessionToken }
124128
}
125129

126130
if (isMultitenant) {
@@ -132,6 +136,7 @@ async function createSignature(
132136
const signature = new SignatureV4({
133137
enforceRegion: s3ProtocolEnforceRegion,
134138
allowForwardedHeader: s3ProtocolAllowForwardedHeader,
139+
nonCanonicalForwardedHost: s3ProtocolNonCanonicalHostHeader,
135140
credentials: {
136141
accessKey: credential.accessKey,
137142
secretKey: credential.secretKey,
@@ -152,6 +157,7 @@ async function createSignature(
152157
const signature = new SignatureV4({
153158
enforceRegion: s3ProtocolEnforceRegion,
154159
allowForwardedHeader: s3ProtocolAllowForwardedHeader,
160+
nonCanonicalForwardedHost: s3ProtocolNonCanonicalHostHeader,
155161
credentials: {
156162
accessKey: s3ProtocolAccessKeyId,
157163
secretKey: s3ProtocolAccessKeySecret,

src/monitoring/logger.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,12 @@ export const logger = pino({
2323
region,
2424
traceId: request.id,
2525
method: request.method,
26-
url: redactQueryParamFromRequest(request, ['token']),
26+
url: redactQueryParamFromRequest(request, [
27+
'token',
28+
'X-Amz-Credential',
29+
'X-Amz-Signature',
30+
'X-Amz-Security-Token',
31+
]),
2732
headers: whitelistHeaders(request.headers),
2833
hostname: request.hostname,
2934
remoteAddress: request.ip,

src/storage/errors.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export enum ErrorCode {
2727
BucketAlreadyExists = 'BucketAlreadyExists',
2828
DatabaseTimeout = 'DatabaseTimeout',
2929
InvalidSignature = 'InvalidSignature',
30+
ExpiredToken = 'ExpiredToken',
3031
SignatureDoesNotMatch = 'SignatureDoesNotMatch',
3132
AccessDenied = 'AccessDenied',
3233
ResourceLocked = 'ResourceLocked',
@@ -145,9 +146,9 @@ export const ERRORS = {
145146

146147
ExpiredSignature: (e?: Error) =>
147148
new StorageBackendError({
148-
code: ErrorCode.InvalidSignature,
149+
code: ErrorCode.ExpiredToken,
149150
httpStatusCode: 400,
150-
message: 'Expired signature',
151+
message: 'The provided token has expired.',
151152
originalError: e,
152153
}),
153154

0 commit comments

Comments
 (0)