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 Jun 15, 2021
1 parent c654ed4 commit 3b0ab3b
Show file tree
Hide file tree
Showing 12 changed files with 635 additions and 140 deletions.
72 changes: 43 additions & 29 deletions packages/cli/src/routeGeneration/templates/express.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { {{name}} } from '{{modulePath}}';
{{/each}}
{{#if authenticationModule}}
import { expressAuthentication } from '{{authenticationModule}}';
// @ts-ignore - no great way to install types from subpackage
const promiseAny = require('promise.any');
{{/if}}
{{#if iocModule}}
import { iocContainer } from '{{iocModule}}';
Expand Down Expand Up @@ -104,51 +106,63 @@ 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
// 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 = function(error: any) {
responded++;
if (responded == security.length && !success) {
error.status = error.status || 401;
next(error)
}
}
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
94 changes: 53 additions & 41 deletions packages/cli/src/routeGeneration/templates/hapi.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { {{name}} } from '{{modulePath}}';
{{/each}}
{{#if authenticationModule}}
import { hapiAuthentication } from '{{authenticationModule}}';
// @ts-ignore - no great way to install types from subpackage
const promiseAny = require('promise.any');
{{/if}}
{{#if iocModule}}
import { iocContainer } from '{{iocModule}}';
Expand Down Expand Up @@ -60,8 +62,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 +137,73 @@ export function RegisterRoutes(server: any) {
{{/each}}

{{#if useSecurity}}

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

const fail = function(error: any) {
responded++;
if (responded == security.length && !success) {
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;
}
return error;
// 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;
};

// 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 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
122 changes: 67 additions & 55 deletions packages/cli/src/routeGeneration/templates/koa.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { {{name}} } from '{{modulePath}}';
{{/each}}
{{#if authenticationModule}}
import { koaAuthentication } from '{{authenticationModule}}';
// @ts-ignore - no great way to install types from subpackage
const promiseAny = require('promise.any');
{{/if}}
{{#if iocModule}}
import { iocContainer } from '{{iocModule}}';
Expand Down Expand Up @@ -103,61 +105,71 @@ 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}}

// 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
1 change: 1 addition & 0 deletions packages/runtime/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"author": "Luke Autry <lukeautry@gmail.com> (http://www.lukeautry.com)",
"license": "MIT",
"dependencies": {
"promise.any": "^2.0.2",
"validator": "^13.6.0"
},
"devDependencies": {
Expand Down

0 comments on commit 3b0ab3b

Please sign in to comment.