Skip to content

Commit

Permalink
ARSN-414: accomodate POST object for auth
Browse files Browse the repository at this point in the history
  • Loading branch information
Will Toozs committed Jun 4, 2024
1 parent a643a3e commit eebc71d
Show file tree
Hide file tree
Showing 5 changed files with 297 additions and 2 deletions.
18 changes: 16 additions & 2 deletions lib/auth/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,13 @@ const checkFunctions = {
v2: {
headers: v2.header.check,
query: v2.query.check,
// TODO CLDSRV-527 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,
},
};

Expand Down Expand Up @@ -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) {
Expand All @@ -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 CLDSRV-527 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
Expand Down Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions lib/auth/v4/authV4.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * as header from './headerAuthCheck';
export * as query from './queryAuthCheck';
export * as form from './formAuthCheck';
198 changes: 198 additions & 0 deletions lib/auth/v4/formAuthCheck.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
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];

// const isTimeSkewed = checkTimeSkew(timestamp, expiry, log);
// if (isTimeSkewed) {
// return { err: errors.RequestTimeTooSkewed };
// }

// 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,
authType: 'REST-QUERY-STRING',
signatureVersion: 'AWS4-HMAC-SHA256',
signatureAge: Date.now() - convertAmzTimeToMs(timestamp),
securityToken: token,
},
},
};
}




import { parse as parseFormData } from 'querystring'; // Node.js module, replace as needed for your setup

/**
* V4 form auth check for POST Object request
* @param request - HTTP request object containing form data
* @param log - logging object
*/
export function checkPostObjectAuth(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 };
}

// - Recreating the canonical request and comparing signatures

// 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,
}
}
};
}
78 changes: 78 additions & 0 deletions lib/auth/v4/validateInputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,84 @@ 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 as this is not
// required for form auth
// NOTE: may be expecting array
authParams.signedHeaders = 'content-type;host;x-amz-date;x-amz-security-token';

// const signedHeaders = formObj['X-Amz-SignedHeaders'];
// // At least "host" must be included in signed headers
// if (signedHeaders && signedHeaders.length > 3) {
// authParams.signedHeaders = signedHeaders;
// } else {
// log.warn('missing signedHeaders');
// return authParams;
// }


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;
}

// 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
Expand Down
4 changes: 4 additions & 0 deletions lib/s3routes/routes/routePOST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

0 comments on commit eebc71d

Please sign in to comment.