diff --git a/lib/auth/auth.ts b/lib/auth/auth.ts index 3344cd69e..ac47f42ec 100644 --- a/lib/auth/auth.ts +++ b/lib/auth/auth.ts @@ -21,10 +21,13 @@ const checkFunctions = { v2: { headers: v2.header.check, query: v2.query.check, + // TODO ARSN-414 check v2 auth for POST requests with form data + // form: v2.form.check, }, v4: { headers: v4.header.check, query: v4.query.check, + form: v4.form.check, }, }; @@ -63,7 +66,7 @@ function extractParams( log.trace('entered', { method: 'Arsenal.auth.server.extractParams' }); const authHeader = request.headers.authorization; let version: 'v2' |'v4' | null = null; - let method: 'query' | 'headers' | null = null; + let method: 'query' | 'headers' | 'form' | null = null; // Identify auth version and method to dispatch to the right check function if (authHeader) { @@ -85,6 +88,16 @@ function extractParams( } else if (data['X-Amz-Algorithm']) { method = 'query'; version = 'v4'; + } if (data.Policy) { + if (data['X-Amz-Algorithm']) { + method = 'form'; + version = 'v4'; + } + // TODO ARSN-414 check v2 auth for POST requests with form data + // if (formData.Signature) { + // method = 'form'; + // version = 'v2'; + // } } // Here, either both values are set, or none is set @@ -121,7 +134,8 @@ function doAuth( awsService: string, requestContexts: any[] | null ) { - const res = extractParams(request, log, awsService, request.query); + const data: { [key: string]: string; } = request.formData || request.query || {}; + const res = extractParams(request, log, awsService, data); if (res.err) { return cb(res.err); } else if (res.params instanceof AuthInfo) { diff --git a/lib/auth/v4/authV4.ts b/lib/auth/v4/authV4.ts index b968aca7d..d3c568b36 100644 --- a/lib/auth/v4/authV4.ts +++ b/lib/auth/v4/authV4.ts @@ -1,2 +1,3 @@ export * as header from './headerAuthCheck'; export * as query from './queryAuthCheck'; +export * as form from './formAuthCheck'; diff --git a/lib/auth/v4/formAuthCheck.ts b/lib/auth/v4/formAuthCheck.ts new file mode 100644 index 000000000..262221a29 --- /dev/null +++ b/lib/auth/v4/formAuthCheck.ts @@ -0,0 +1,187 @@ +import { Logger } from 'werelogs'; +import * as constants from '../../constants'; +import errors from '../../errors'; +import constructStringToSign from './constructStringToSign'; +import { checkTimeSkew, convertAmzTimeToMs } from './timeUtils'; +import { validateCredentials, extractFormParams } from './validateInputs'; +import { areSignedHeadersComplete } from './validateInputs'; + +/** + * V4 query auth check + * @param request - HTTP request object + * @param log - logging object + * @param data - Contain authentification params (GET or POST data) + */ +export function check(request: any, log: Logger, data: { [key: string]: string }) { + const authParams = extractFormParams(data, log); + + if (Object.keys(authParams).length !== 4) { + return { err: errors.InvalidArgument }; + } + + // Query params are not specified in AWS documentation as case-insensitive, + // so we use case-sensitive + const token = data['X-Amz-Security-Token']; + if (token && !constants.iamSecurityToken.pattern.test(token)) { + log.debug('invalid security token', { token }); + return { err: errors.InvalidToken }; + } + + const signedHeaders = authParams.signedHeaders!; + const signatureFromRequest = authParams.signatureFromRequest!; + const timestamp = authParams.timestamp!; + //const expiry = authParams.expiry!; + const credential = authParams.credential!; + + if (!areSignedHeadersComplete(signedHeaders, request.headers)) { + log.debug('signedHeaders are incomplete', { signedHeaders }); + return { err: errors.AccessDenied }; + } + + const validationResult = validateCredentials(credential, timestamp, + log); + if (validationResult instanceof Error) { + log.debug('credentials in improper format', { credential, + timestamp, validationResult }); + return { err: validationResult }; + } + const accessKey = credential[0]; + const scopeDate = credential[1]; + const region = credential[2]; + const service = credential[3]; + const requestType = credential[4]; + + // In query v4 auth, the canonical request needs + // to include the query params OTHER THAN + // the signature so create a + // copy of the query object and remove + // the X-Amz-Signature property. + const queryWithoutSignature = Object.assign({}, data); + delete queryWithoutSignature['X-Amz-Signature']; + + // For query auth, instead of a + // checksum of the contents, the + // string 'UNSIGNED-PAYLOAD' should be + // added to the canonicalRequest in + // building string to sign + const payloadChecksum = 'UNSIGNED-PAYLOAD'; + + // string to sign is the policy + const stringToSign = data['Policy']; + log.trace('constructed stringToSign', { stringToSign }); + return { + err: null, + params: { + version: 4, + data: { + accessKey, + signatureFromRequest, + region, + scopeDate, + stringToSign, + service, + authType: 'REST-QUERY-STRING', + signatureVersion: 'AWS4-HMAC-SHA256', + signatureAge: Date.now() - convertAmzTimeToMs(timestamp), + securityToken: token, + }, + }, + }; +} + +// /** +// * V4 form auth check for POST Object request +// * @param request - HTTP request object containing form data +// * @param log - logging object +// */ +// export function check(request: any, log: Logger, formData: { [key: string]: string }) { +// // Assume form data is already parsed and attached to request.body + +// // Extract authentication parameters from formData +// const algorithm = formData['X-Amz-Algorithm']; +// const credentials = formData['X-Amz-Credential']; +// const date = formData['X-Amz-Date']; +// const securityToken = formData['X-Amz-Security-Token']; +// const signature = formData['X-Amz-Signature']; + +// let splitCredentials : [string, string, string, string, string]; +// if (credentials && credentials.length > 28 && credentials.indexOf('/') > -1) { +// // @ts-ignore +// splitCredentials = credentials.split('/'); +// } else { +// log.debug('invalid credential param', { credentials, +// date }); +// return { err: errors.InvalidArgument }; +// } + +// if (!algorithm || !splitCredentials || !date || !signature) { +// return { err: errors.InvalidArgument }; +// } + +// // Validate the token if present +// if (securityToken && !constants.iamSecurityToken.pattern.test(securityToken)) { +// log.debug('invalid security token', { token: securityToken }); +// return { err: errors.InvalidToken }; +// } + +// // Checking credential format +// const validationResult = validateCredentials(splitCredentials, date, +// log); +// if (validationResult instanceof Error) { +// log.debug('credentials in improper format', { splitCredentials, +// date, validationResult }); +// return { err: validationResult }; +// } + +// const accessKey = splitCredentials[0]; +// const scopeDate = splitCredentials[1]; +// const region = splitCredentials[2]; +// const service = splitCredentials[3]; +// const requestType = splitCredentials[4]; + +// // Verifying the timestamp and potential expiration +// const isTimeSkewed = checkTimeSkew(date, request.expiry, log); +// if (isTimeSkewed) { +// return { err: errors.RequestTimeTooSkewed }; +// } + +// // Extract signed headers +// const signedHeaders = Object.keys(request.headers).map(key => key.toLowerCase()).sort().join(';'); + + +// const stringToSign = constructStringToSign({ +// request, +// signedHeaders, +// payloadChecksum: null, +// credentialScope: +// `${scopeDate}/${region}/${service}/${requestType}`, +// timestamp: date, +// query: formData, +// log, +// awsService: service, +// }); +// if (stringToSign instanceof Error) { +// return { err: stringToSign }; +// } +// log.trace('constructed stringToSign', { stringToSign }); + +// // If all checks are successful +// return { +// err: null, +// params: { +// version: 4, +// data: { +// accessKey: accessKey, +// signatureFromRequest: signature, +// date: date, +// region: region, +// scopeDate: scopeDate, +// stringToSign: stringToSign, +// authType: 'POST-OBJECT', +// signatureVersion: 'AWS4-HMAC-SHA256', +// signatureAge: Date.now() - convertAmzTimeToMs(date), +// securityToken: securityToken, +// } +// } +// }; +// } diff --git a/lib/auth/v4/validateInputs.ts b/lib/auth/v4/validateInputs.ts index 4d83fba24..60193d55d 100644 --- a/lib/auth/v4/validateInputs.ts +++ b/lib/auth/v4/validateInputs.ts @@ -131,6 +131,73 @@ export function extractQueryParams( } +/** + * Extract and validate components from formData object + * @param formObj - formData object from request + * @param log - logging object + * @return object containing extracted query params for authV4 + */ +export function extractFormParams( + formObj: { [key: string]: string | undefined }, + log: Logger +) { + const authParams: { + signedHeaders?: string; + signatureFromRequest?: string; + timestamp?: string; + expiry?: number; + credential?: [string, string, string, string, string]; + } = {}; + + // Do not need the algorithm sent back + if (formObj['X-Amz-Algorithm'] !== 'AWS4-HMAC-SHA256') { + log.warn('algorithm param incorrect', + { algo: formObj['X-Amz-Algorithm'] }); + return authParams; + } + + // adding placeholder for signedHeaders to satisfy Vault + // as this is not required for form auth + authParams.signedHeaders = 'content-type;host;x-amz-date;x-amz-security-token'; + + const signature = formObj['X-Amz-Signature']; + if (signature && signature.length === 64) { + authParams.signatureFromRequest = signature; + } else { + log.warn('missing signature'); + return authParams; + } + + const timestamp = formObj['X-Amz-Date']; + if (timestamp && timestamp.length === 16) { + authParams.timestamp = timestamp; + } else { + log.warn('missing or invalid timestamp', + { timestamp: formObj['X-Amz-Date'] }); + return authParams; + } + + // TODO? ARSN-414 Does not seem to be required for form auth + // const expiry = Number.parseInt(formObj['X-Amz-Expires'] ?? 'nope', 10); + // const sevenDays = 604800; + // if (expiry && (expiry > 0 && expiry <= sevenDays)) { + // authParams.expiry = expiry; + // } else { + // log.warn('invalid expiry', { expiry }); + // return authParams; + // } + + const credential = formObj['X-Amz-Credential']; + if (credential && credential.length > 28 && credential.indexOf('/') > -1) { + // @ts-ignore + authParams.credential = credential.split('/'); + } else { + log.warn('invalid credential param', { credential }); + return authParams; + } + return authParams; +} + /** * Extract and validate components from auth header * @param authHeader - authorization header from request diff --git a/lib/s3routes/routes/routePOST.ts b/lib/s3routes/routes/routePOST.ts index 1854d4b97..835d09915 100644 --- a/lib/s3routes/routes/routePOST.ts +++ b/lib/s3routes/routes/routePOST.ts @@ -58,6 +58,10 @@ export default function routePOST( corsHeaders)); } + if (objectKey === undefined && Object.keys(query).length === 0) { + return api.callApiMethod('objectPost', request, response, log, (err, resHeaders) => routesUtils.responseNoBody(err, resHeaders, response, 200, log)); + } + return routesUtils.responseNoBody(errors.NotImplemented, null, response, 200, log); }