Skip to content

Commit

Permalink
fix security handling of multiple auth types for hapi/koa
Browse files Browse the repository at this point in the history
As part of this refactored how calls to the authentication method for each
server type are handled.  Express multi auth was not previously broken
but I was able to share the same solution across all three servers and
isolate the server specific handling to a single block of code after
waiting for the appropriate promises to be resolved.

I additionally added tests for the following:
- security AND / OR tests for hapi/koa
- a slow failure test to show that promiseAny returns with first success
- checks for which error is resolved to the user in the case that there
  is more than one

Closes #974
  • Loading branch information
fantapop committed May 18, 2021
1 parent 22cb926 commit 29fae38
Show file tree
Hide file tree
Showing 10 changed files with 419 additions and 141 deletions.
80 changes: 51 additions & 29 deletions packages/cli/src/routeGeneration/templates/express.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -104,51 +104,73 @@ export function RegisterRoutes(app: express.Router) {
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa

{{#if useSecurity}}
function authenticateMiddleware(security: TsoaRoute.Security[] = []) {
return function runAuthenticationMiddleware(request: any, _response: any, next: any) {
let responded = 0;
let success = false;

const succeed = function(user: any) {
if (!success) {
success = true;
responded++;
request['user'] = user;
next();
}
}

// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
function reverse<T>(promise: Promise<T>) {
return new Promise((resolve, reject) => Promise.resolve(promise).then(reject, resolve));
}

const fail = function(error: any) {
responded++;
if (responded == security.length && !success) {
error.status = error.status || 401;
next(error)
}
}
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa

function promiseAny<T>(iterable: Iterable<Promise<T>>) {
return reverse(Promise.all([...iterable].map(reverse)));
};

// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa

function authenticateMiddleware(security: TsoaRoute.Security[] = []) {
return async function runAuthenticationMiddleware(request: any, _response: any, next: any) {

// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa

// keep track of failed auth attempts so we can hand back the most
// recent one. This behavior was previously existing so preserving it
// here
const failedAttempts: any[] = [];
const pushAndRethrow = (error: any) => {
failedAttempts.push(error);
throw error;
};

const secMethodOrPromises: Promise<any>[] = [];
for (const secMethod of security) {
if (Object.keys(secMethod).length > 1) {
let promises: Promise<any>[] = [];
const secMethodAndPromises: Promise<any>[] = [];

for (const name in secMethod) {
promises.push(expressAuthentication(request, name, secMethod[name]));
secMethodAndPromises.push(
expressAuthentication(request, name, secMethod[name])
.catch(pushAndRethrow)
);
}

Promise.all(promises)
.then((users) => { succeed(users[0]); })
.catch(fail);
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa

secMethodOrPromises.push(Promise.all(secMethodAndPromises)
.then(users => { return users[0]; }));
} else {
for (const name in secMethod) {
expressAuthentication(request, name, secMethod[name])
.then(succeed)
.catch(fail);
secMethodOrPromises.push(
expressAuthentication(request, name, secMethod[name])
.catch(pushAndRethrow)
);
}
}
}

// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa

try {
request['user'] = await promiseAny(secMethodOrPromises);
next();
}
catch(err) {
// Show most recent error as response
const error = failedAttempts.pop();
error.status = error.status || 401;
next(error);
}

// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
}
}
{{/if}}
Expand Down
102 changes: 61 additions & 41 deletions packages/cli/src/routeGeneration/templates/hapi.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,7 @@ export function RegisterRoutes(server: any) {
pre: [
{{#if security.length}}
{
method: authenticateMiddleware({{json security}}),
assign: "user"
method: authenticateMiddleware({{json security}})
},
{{/if}}
{{#if uploadFile}}
Expand Down Expand Up @@ -136,62 +135,83 @@ export function RegisterRoutes(server: any) {
{{/each}}

{{#if useSecurity}}
function authenticateMiddleware(security: TsoaRoute.Security[] = []) {
return function runAuthenticationMiddleware(request: any, h: any) {
let responded = 0;
let success = false;

const succeed = function(user: any) {
if (!success) {
success = true;
responded++;
request['user'] = user;
}
return user;
};

// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
function reverse<T>(promise: Promise<T>) {
return new Promise((resolve, reject) => Promise.resolve(promise).then(reject, resolve));
}

const fail = function(error: any) {
responded++;
if (responded == security.length && !success) {
if (isBoom(error)) {
throw error;
}
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa

const boomErr = boomify(error instanceof Error ? error : new Error(error.message));
boomErr.output.statusCode = error.status || 401;
boomErr.output.payload = {
name: error.name,
message: error.message,
} as unknown as Payload;
throw boomErr;
}
return error;
};
function promiseAny<T>(iterable: Iterable<Promise<T>>) {
return reverse(Promise.all([...iterable].map(reverse)));
};

// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa

function authenticateMiddleware(security: TsoaRoute.Security[] = []) {
return async function runAuthenticationMiddleware(request: any, h: any) {

// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa

// keep track of failed auth attempts so we can hand back the most
// recent one. This behavior was previously existing so preserving it
// here
const failedAttempts: any[] = [];
const pushAndRethrow = (error: any) => {
failedAttempts.push(error);
throw error;
};

const secMethodOrPromises: Promise<any>[] = [];
for (const secMethod of security) {
if (Object.keys(secMethod).length > 1) {
let promises: Promise<any>[] = [];
const secMethodAndPromises: Promise<any>[] = [];

for (const name in secMethod) {
promises.push(hapiAuthentication(request, name, secMethod[name]));
secMethodAndPromises.push(
hapiAuthentication(request, name, secMethod[name])
.catch(pushAndRethrow)
);
}

return Promise.all(promises)
.then((users) => { succeed(users[0]); })
.catch(fail);
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa

secMethodOrPromises.push(Promise.all(secMethodAndPromises)
.then(users => { return users[0]; }));
} else {
for (const name in secMethod) {
return hapiAuthentication(request, name, secMethod[name])
.then(succeed)
.catch(fail);
secMethodOrPromises.push(
hapiAuthentication(request, name, secMethod[name])
.catch(pushAndRethrow)
);
}
}
}
return null;

// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa

try {
request['user'] = await promiseAny(secMethodOrPromises);
return request['user'];
}
catch(err) {
// Show most recent error as response
const error = failedAttempts.pop();
if (isBoom(error)) {
throw error;
}

const boomErr = boomify(error instanceof Error ? error : new Error(error.message));
boomErr.output.statusCode = error.status || 401;
boomErr.output.payload = {
name: error.name,
message: error.message,
} as unknown as Payload;

throw boomErr;
}

// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
}
}
{{/if}}
Expand Down
130 changes: 75 additions & 55 deletions packages/cli/src/routeGeneration/templates/koa.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -103,61 +103,81 @@ export function RegisterRoutes(router: KoaRouter) {
{{/each}}
{{/each}}

{{#if useSecurity}}
function authenticateMiddleware(security: TsoaRoute.Security[] = []) {
return async function runAuthenticationMiddleware(context: any, next: any) {
let responded = 0;
let success = false;

const succeed = async (user: any) => {
if (!success) {
success = true;
responded++;
context.request['user'] = user;
await next();
}
};

// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa

const fail = async (error: any) => {
responded++;
if (responded == security.length && !success) {
// this is an authentication error
context.status = error.status || 401;
context.throw(context.status, error.message, error);
} else if (success) {
// the authentication was a success but arriving here means the controller
// probably threw an error that we caught as well
// so just pass it on
throw error;
}
};

// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa

for (const secMethod of security) {
if (Object.keys(secMethod).length > 1) {
let promises: Promise<any>[] = [];

for (const name in secMethod) {
promises.push(koaAuthentication(context.request, name, secMethod[name]));
}

return Promise.all(promises)
.then((users) => succeed(users[0]))
.catch(fail);
} else {
for (const name in secMethod) {
return koaAuthentication(context.request, name, secMethod[name])
.then(succeed)
.catch(fail);
}
}
}
}
}
{{/if}}
{{#if useSecurity}}

function reverse<T>(promise: Promise<T>) {
return new Promise((resolve, reject) => Promise.resolve(promise).then(reject, resolve));
}

// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa

function promiseAny<T>(iterable: Iterable<Promise<T>>) {
return reverse(Promise.all([...iterable].map(reverse)));
};

// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa

function authenticateMiddleware(security: TsoaRoute.Security[] = []) {
return async function runAuthenticationMiddleware(context: any, next: any) {

// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa

// keep track of failed auth attempts so we can hand back the most
// recent one. This behavior was previously existing so preserving it
// here
const failedAttempts: any[] = [];
const pushAndRethrow = (error: any) => {
failedAttempts.push(error);
throw error;
};

const secMethodOrPromises: Promise<any>[] = [];
for (const secMethod of security) {
if (Object.keys(secMethod).length > 1) {
const secMethodAndPromises: Promise<any>[] = [];

for (const name in secMethod) {
secMethodAndPromises.push(
koaAuthentication(context.request, name, secMethod[name])
.catch(pushAndRethrow)
);
}

secMethodOrPromises.push(Promise.all(secMethodAndPromises)
.then(users => { return users[0]; }));
} else {
for (const name in secMethod) {
secMethodOrPromises.push(
koaAuthentication(context.request, name, secMethod[name])
.catch(pushAndRethrow)
);
}
}
}

// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa

let success;
try {
const user = await promiseAny(secMethodOrPromises);
success = true;
context.request['user'] = user;
}
catch(err) {
// Show most recent error as response
const error = failedAttempts.pop();
context.status = error.status || 401;
context.throw(context.status, error.message, error);
}

if (success) {
await next();
}

// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
}
}
{{/if}}

// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa

Expand Down

0 comments on commit 29fae38

Please sign in to comment.