зеркало из https://github.com/microsoft/CCF.git
@Security decorator support for forum sample / tsoa apps (#1824)
This commit is contained in:
Родитель
5305e01991
Коммит
a583161f97
|
@ -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"
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче