handle auth errors in error handler (#1827)

This commit is contained in:
Maik Riechert 2020-10-27 18:09:23 +01:00 коммит произвёл GitHub
Родитель 03028afe48
Коммит c81b7d4277
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
4 изменённых файлов: 84 добавлений и 81 удалений

Просмотреть файл

@ -3,28 +3,35 @@
import jwt_decode from 'jwt-decode'
import * as ccf from './types/ccf'
import { UnauthorizedError } from './error_handler'
export function authentication(request: ccf.Request, securityName: string, scopes?: string[]): any {
export interface User {
claims: { [name: string]: any }
userId: string
}
export function authentication(request: ccf.Request, securityName: string, scopes?: string[]): void {
if (securityName === "jwt") {
const authHeader = request.headers['authorization']
if (!authHeader) {
throw new Error('authorization header missing')
throw new UnauthorizedError('authorization header missing')
}
const parts = authHeader.split(' ', 2)
if (parts.length !== 2 || parts[0] !== 'Bearer') {
throw new Error('unexpected authentication type')
throw new UnauthorizedError('unexpected authentication type')
}
const token = parts[1]
let claims: any
try {
claims = jwt_decode(token)
} catch (e) {
throw new Error(`malformed jwt: ${e.message}`)
throw new UnauthorizedError(`malformed jwt: ${e.message}`)
}
return {
request.user = {
claims: claims,
userId: claims.sub
}
} as User
} else {
throw new Error(`BUG: unknown securityName: ${securityName}`)
}
throw new Error(`BUG: unknown securityName: ${securityName}`)
}

Просмотреть файл

@ -20,10 +20,10 @@ import * as _ from 'lodash-es'
import * as math from 'mathjs'
import {
ErrorResponse, ValidateErrorResponse, ValidateErrorStatus,
BadRequestError, ForbiddenError, NotFoundError
ErrorResponse, ValidateErrorResponse, ValidateError,
BadRequestError, ForbiddenError, NotFoundError, UnauthorizedError
} from "../error_handler"
import { parseAuthToken } from "../util"
import { User } from "../authentication"
import * as ccf from "../types/ccf"
export const MINIMUM_OPINION_THRESHOLD = 10
@ -106,6 +106,8 @@ namespace kv {
@Route("polls")
@Security("jwt")
@Response<ErrorResponse>(UnauthorizedError.Status, "Unauthorized")
@Response<ValidateErrorResponse>(ValidateError.Status, "Schema validation error")
export class PollController extends Controller {
private kvPolls = new ccf.TypedKVMap(ccf.kv.polls, ccf.string, ccf.json<kv.Poll>())
@ -114,20 +116,19 @@ export class PollController extends Controller {
@SuccessResponse(201, "Poll has been successfully created")
@Response<ErrorResponse>(ForbiddenError.Status, "Poll has not been created because a poll with the same topic exists already")
@Response<ValidateErrorResponse>(ValidateErrorStatus, "Schema validation error")
@Post('{topic}')
public createPoll(
@Path() topic: string,
@Body() body: CreatePollRequest,
@Request() request: ccf.Request
): void {
const user = request.user.userId
const user: User = request.user
if (this.kvPolls.has(topic)) {
throw new ForbiddenError("Poll with given topic exists already")
}
this.kvPolls.set(topic, {
creator: user,
creator: user.userId,
type: body.type,
opinions: {}
})
@ -139,20 +140,19 @@ export class PollController extends Controller {
@SuccessResponse(201, "Polls have been successfully created")
@Response<ErrorResponse>(ForbiddenError.Status, "Polls were not created because a poll with the same topic exists already")
@Response<ValidateErrorResponse>(ValidateErrorStatus, "Schema validation error")
@Post()
public createPolls(
@Body() body: CreatePollsRequest,
@Request() request: ccf.Request
): void {
const user = request.user.userId
const user: User = request.user
for (let [topic, poll] of Object.entries(body.polls)) {
if (this.kvPolls.has(topic)) {
throw new ForbiddenError(`Poll with topic '${topic}' exists already`)
}
this.kvPolls.set(topic, {
creator: user,
creator: user.userId,
type: poll.type,
opinions: {}
})
@ -166,14 +166,13 @@ export class PollController extends Controller {
@SuccessResponse(204, "Opinion has been successfully recorded")
@Response<ErrorResponse>(BadRequestError.Status, "Opinion was not recorded because the opinion data type does not match the poll type")
@Response<ErrorResponse>(NotFoundError.Status, "Opinion was not recorded because no poll with the given topic exists")
@Response<ValidateErrorResponse>(ValidateErrorStatus, "Schema validation error")
@Put('{topic}')
public submitOpinion(
@Path() topic: string,
@Body() body: SubmitOpinionRequest,
@Request() request: ccf.Request
): void {
const user = request.user.userId
const user: User = request.user
const poll = this.kvPolls.get(topic)
if (poll === undefined) {
@ -182,20 +181,19 @@ export class PollController extends Controller {
if (typeof body.opinion !== poll.type) {
throw new BadRequestError("Poll has a different opinion type")
}
poll.opinions[user] = body.opinion
poll.opinions[user.userId] = body.opinion
this.kvPolls.set(topic, poll)
this.setStatus(204)
}
@SuccessResponse(204, "Opinions have been successfully recorded")
@Response<ErrorResponse>(BadRequestError.Status, "Opinions were not recorded because either an opinion data type did not match the poll type or a poll with the given topic was not found")
@Response<ValidateErrorResponse>(ValidateErrorStatus, "Schema validation error")
@Put()
public submitOpinions(
@Body() body: SubmitOpinionsRequest,
@Request() request: ccf.Request
): void {
const user = request.user.userId
const user: User = request.user
for (const [topic, opinion] of Object.entries(body.opinions)) {
const poll = this.kvPolls.get(topic)
@ -205,7 +203,7 @@ export class PollController extends Controller {
if (typeof opinion.opinion !== poll.type) {
throw new BadRequestError(`Poll with topic '${topic}' has a different opinion type`)
}
poll.opinions[user] = opinion.opinion
poll.opinions[user.userId] = opinion.opinion
this.kvPolls.set(topic, poll)
}
@ -214,35 +212,32 @@ export class PollController extends Controller {
@SuccessResponse(200, "Poll data")
@Response<ErrorResponse>(NotFoundError.Status, "Poll data could not be returned because no poll with the given topic exists")
@Response<ValidateErrorResponse>(ValidateErrorStatus, "Schema validation error")
@Get('{topic}')
public getPoll(
@Path() topic: string,
@Request() request: ccf.Request
): GetPollResponse {
const user = request.user.userId
const user: User = request.user
if (!this.kvPolls.has(topic)){
throw new NotFoundError("Poll does not exist")
}
this.setStatus(200)
return this._getPoll(user, topic)
return this._getPoll(user.userId, topic)
}
@SuccessResponse(200, "Poll data")
@Response<ValidateErrorResponse>(ValidateErrorStatus, "Schema validation error")
@Get()
public getPolls(
@Header() authorization: string,
@Request() request: ccf.Request
): GetPollsResponse {
const user = request.user.userId
const user: User = request.user
let response: GetPollsResponse = { polls: {} }
for (const topic of this._getTopics()) {
response.polls[topic] = this._getPoll(user, topic)
response.polls[topic] = this._getPoll(user.userId, topic)
}
this.setStatus(200)

Просмотреть файл

@ -1,16 +1,9 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the Apache 2.0 License.
import { ValidateError, FieldErrors } from "@tsoa/runtime";
import { ValidateError as TsoaValidateError, FieldErrors } from "@tsoa/runtime";
import * as ccf from './types/ccf'
// The global error handler. Gets called for:
// - Request schema validation errors
// - Uncaught exceptions in controller actions
// See https://tsoa-community.github.io/docs/error-handling.html#setting-up-error-handling
// The code that imports and calls this handler is in tsoa-support/routes.ts.tmpl.
export interface ErrorResponse {
message: string
}
@ -20,7 +13,9 @@ export interface ValidateErrorResponse extends ErrorResponse {
details: FieldErrors
}
export const ValidateErrorStatus = 422
export abstract class ValidateError {
static Status = 422
}
class HttpError extends Error {
constructor(public statusCode: number, message: string) {
@ -36,6 +31,14 @@ export class BadRequestError extends HttpError {
}
}
export class UnauthorizedError extends HttpError {
static Status = 401
constructor(message: string) {
super(UnauthorizedError.Status, message)
}
}
export class ForbiddenError extends HttpError {
static Status = 403
@ -52,14 +55,24 @@ export class NotFoundError extends HttpError {
}
}
/** The global error handler.
*
* This handler is called for:
* - Request schema validation errors
* - Exceptions thrown by the authentication module
* - Uncaught exceptions in controller actions
*
* See https://tsoa-community.github.io/docs/error-handling.html#setting-up-error-handling
* The code that imports and calls this handler is in tsoa-support/routes.ts.tmpl.
*/
export function errorHandler(err: unknown, req: ccf.Request): ccf.Response<ErrorResponse | ValidateErrorResponse> {
if (err instanceof ValidateError) {
if (err instanceof TsoaValidateError) {
return {
body: {
message: "Validation failed",
details: err.fields
},
statusCode: ValidateErrorStatus
statusCode: ValidateError.Status
}
} else if (err instanceof HttpError) {
return {

Просмотреть файл

@ -113,14 +113,13 @@ export function getValidatedArgs(args: any, request: any, response: any): any[]
}
{{#if useSecurity}}
export function authenticateMiddleware(security: TsoaRoute.Security[], request: any, response: any) {
let user = undefined
export function authenticateMiddleware(security: TsoaRoute.Security[], request: any) {
let success = false
let error = undefined
for (const secMethod of security) {
for (const name in secMethod) {
try {
user = authentication(request, name, secMethod[name])
authentication(request, name, secMethod[name])
success = true
break
} catch (e) {
@ -131,17 +130,8 @@ export function authenticateMiddleware(security: TsoaRoute.Security[], request:
break
}
}
if (success) {
request['user'] = user
return true
} else {
response.statusCode = 401
if (error.message) {
response.body = {
message: error.message
}
}
return false
if (!success) {
throw error
}
}
{{/if}}
@ -187,34 +177,32 @@ import { {{name}} } from '{{modulePath}}';
{{#each actions}}
// {{method}} {{fullPath}}
export function {{name}}(request) {
const response = {
body: undefined,
statusCode: undefined,
headers: {}
}
{{#if security.length}}
if (!authenticateMiddleware({{json security}}, request, response)) {
return response;
}
{{/if}}
const args = {
{{#each parameters}}
{{@key}}: {{{json this}}},
{{/each}}
};
const request_ = {
body: request.body,
headers: request.headers,
query: parseQuery(request.query),
params: request.params,
user: request.user
}
try {
let validatedArgs = getValidatedArgs(args, request_, response);
{{#if security.length}}
authenticateMiddleware({{json security}}, request)
{{/if}}
const args = {
{{#each parameters}}
{{@key}}: {{{json this}}},
{{/each}}
};
const request_ = {
body: request.body,
headers: request.headers,
query: parseQuery(request.query),
params: request.params,
user: request.user
}
const response = {
body: undefined,
statusCode: undefined,
headers: {}
}
const validatedArgs = getValidatedArgs(args, request_, response);
const controller = new {{../name}}();