@Security decorator support for forum sample / tsoa apps (#1824)

This commit is contained in:
Maik Riechert 2020-10-27 12:04:41 +01:00 коммит произвёл GitHub
Родитель 5305e01991
Коммит a583161f97
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
7 изменённых файлов: 110 добавлений и 51 удалений

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

@ -0,0 +1,30 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the Apache 2.0 License.
import jwt_decode from 'jwt-decode'
import * as ccf from './types/ccf'
export function authentication(request: ccf.Request, securityName: string, scopes?: string[]): any {
if (securityName === "jwt") {
const authHeader = request.headers['authorization']
if (!authHeader) {
throw new Error('authorization header missing')
}
const parts = authHeader.split(' ', 2)
if (parts.length !== 2 || parts[0] !== 'Bearer') {
throw new Error('unexpected authentication type')
}
const token = parts[1]
let claims: any
try {
claims = jwt_decode(token)
} catch (e) {
throw new Error(`malformed jwt: ${e.message}`)
}
return {
claims: claims,
userId: claims.sub
}
}
throw new Error(`BUG: unknown securityName: ${securityName}`)
}

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

@ -6,8 +6,10 @@ import {
Path,
Header,
SuccessResponse,
Request,
Response,
Controller,
Security,
Get,
Post,
Put,
@ -18,7 +20,6 @@ import * as _ from 'lodash-es'
import * as math from 'mathjs'
import { ValidateErrorResponse, ValidateErrorStatus } from "../error_handler"
import { parseAuthToken } from "../util"
import * as ccf from "../types/ccf"
export const MINIMUM_OPINION_THRESHOLD = 10
@ -104,6 +105,7 @@ namespace kv {
@Route("polls")
@Security("jwt")
export class PollController extends Controller {
private kvPolls = new ccf.TypedKVMap(ccf.kv.polls, ccf.string, ccf.json<kv.Poll>())
@ -117,9 +119,9 @@ export class PollController extends Controller {
public createPoll(
@Path() topic: string,
@Body() body: CreatePollRequest,
@Header() authorization: string,
@Request() request: ccf.Request
): void {
const user = parseAuthToken(authorization)
const user = request.user.userId
if (this.kvPolls.has(topic)) {
this.setStatus(403)
@ -142,9 +144,9 @@ export class PollController extends Controller {
@Post()
public createPolls(
@Body() body: CreatePollsRequest,
@Header() authorization: string,
@Request() request: ccf.Request
): void {
const user = parseAuthToken(authorization)
const user = request.user.userId
for (let [topic, poll] of Object.entries(body.polls)) {
if (this.kvPolls.has(topic)) {
@ -171,9 +173,9 @@ export class PollController extends Controller {
public submitOpinion(
@Path() topic: string,
@Body() body: SubmitOpinionRequest,
@Header() authorization: string,
@Request() request: ccf.Request
): void {
const user = parseAuthToken(authorization)
const user = request.user.userId
const poll = this.kvPolls.get(topic)
if (poll === undefined) {
@ -195,9 +197,9 @@ export class PollController extends Controller {
@Put()
public submitOpinions(
@Body() body: SubmitOpinionsRequest,
@Header() authorization: string,
@Request() request: ccf.Request
): void {
const user = parseAuthToken(authorization)
const user = request.user.userId
for (const [topic, opinion] of Object.entries(body.opinions)) {
const poll = this.kvPolls.get(topic)
@ -222,9 +224,9 @@ export class PollController extends Controller {
@Get('{topic}')
public getPoll(
@Path() topic: string,
@Header() authorization: string,
@Request() request: ccf.Request
): GetPollResponse {
const user = parseAuthToken(authorization)
const user = request.user.userId
if (!this.kvPolls.has(topic)){
this.setStatus(404)
@ -240,8 +242,9 @@ export class PollController extends Controller {
@Get()
public getPolls(
@Header() authorization: string,
@Request() request: ccf.Request
): GetPollsResponse {
const user = parseAuthToken(authorization)
const user = request.user.userId
let response: GetPollsResponse = { polls: {} }

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

@ -19,6 +19,7 @@ export interface Request<T extends JsonCompatible<T> = any> {
params: { [key: string]: string; }
query: string
body: Body<T>
user?: any
}
type ResponseBodyType<T> = string | ArrayBuffer | JsonCompatible<T>

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

@ -1,18 +0,0 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the Apache 2.0 License.
import jwt_decode from 'jwt-decode'
export function parseAuthToken(authHeader: string): string {
const parts = authHeader.split(' ', 2)
if (parts.length !== 2 || parts[0] !== 'Bearer') {
throw new Error('unexpected authorization type')
}
const token = parts[1]
const jwt = jwt_decode(token) as any
const user = jwt.sub
if (!user) {
throw new Error('invalid jwt, "sub" claim not found')
}
return user
}

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

@ -61,8 +61,7 @@ describe('/polls', function () {
const body: CreatePollRequest = {
type: "string"
}
// 422 = validation error, because the header is missing, should be 401
await bent('POST', 422)(`${ENDPOINT_URL}/${topic}`, body)
await bent('POST', 401)(`${ENDPOINT_URL}/${topic}`, body)
})
})
describe('POST /', function () {
@ -90,8 +89,7 @@ describe('/polls', function () {
'post-multiple-d': { type: "number" }
}
}
// 422 = validation error, because the header is missing, should be 401
await bent('POST', 422)(`${ENDPOINT_URL}`, body)
await bent('POST', 401)(`${ENDPOINT_URL}`, body)
})
})
describe('PUT /{topic}', function () {
@ -135,8 +133,7 @@ describe('/polls', function () {
const opinionBody: SubmitOpinionRequest = {
opinion: 1.2
}
// 422 = validation error, because the header is missing, should be 401
await bent('PUT', 422)(`${ENDPOINT_URL}/${topic}`, opinionBody)
await bent('PUT', 401)(`${ENDPOINT_URL}/${topic}`, opinionBody)
})
})
describe('PUT /', function () {
@ -200,8 +197,7 @@ describe('/polls', function () {
[topic]: { opinion: 1.5 }
}
}
// 422 = validation error, because the header is missing, should be 401
await bent('PUT', 422)(`${ENDPOINT_URL}`, opinionBody)
await bent('PUT', 401)(`${ENDPOINT_URL}`, opinionBody)
})
})
describe('GET /{topic}', function () {

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

@ -7,14 +7,11 @@
import { Controller, ValidationService, FieldErrors, ValidateError, TsoaRoute, HttpStatusCodeLiteral, TsoaResponse } from '@tsoa/runtime';
{{#if authenticationModule}}
UNSUPPORTED: authenticationModule
import { authentication } from '{{authenticationModule}}';
{{/if}}
{{#if iocModule}}
UNSUPPORTED: iocModule
{{/if}}
{{#if useSecurity}}
UNSUPPORTED: useSecurity
{{/if}}
const models: TsoaRoute.Models = {
{{#each models}}
@ -115,6 +112,40 @@ export function getValidatedArgs(args: any, request: any, response: any): any[]
return values;
}
{{#if useSecurity}}
export function authenticateMiddleware(security: TsoaRoute.Security[], request: any, response: any) {
let user = undefined
let success = false
let error = undefined
for (const secMethod of security) {
for (const name in secMethod) {
try {
user = authentication(request, name, secMethod[name])
success = true
break
} catch (e) {
error = e
}
}
if (success) {
break
}
}
if (success) {
request['user'] = user
return true
} else {
response.statusCode = 401
if (error.message) {
response.body = {
message: error.message
}
}
return false
}
}
{{/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
// CCF:HELPERS-END
@ -147,7 +178,7 @@ export function getValidatedArgs(args: any, request: any, response: any): any[]
/* eslint-disable */
// 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
import { parseQuery, getValidatedArgs, promiseHandler } from './helpers'
import { parseQuery, getValidatedArgs, promiseHandler, authenticateMiddleware } from './helpers'
import { errorHandler } from '../src/error_handler'
import { {{name}} } from '{{modulePath}}';
@ -156,6 +187,18 @@ 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}}},
@ -166,12 +209,8 @@ export function {{name}}(request) {
body: request.body,
headers: request.headers,
query: parseQuery(request.query),
params: request.params
}
const response = {
body: undefined,
statusCode: undefined,
headers: {}
params: request.params,
user: request.user
}
try {

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

@ -7,10 +7,18 @@
"spec": {
"outputDirectory": "build",
"specVersion": 3,
"basePath": "/app"
"basePath": "/app",
"securityDefinitions": {
"jwt": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT"
}
}
},
"routes": {
"routesDir": "build",
"middlewareTemplate": "tsoa-support/routes.ts.tmpl"
"middlewareTemplate": "tsoa-support/routes.ts.tmpl",
"authenticationModule": "./src/authentication.ts"
}
}