From c3096472ef2a5b43de0e0a47b1910938f3a0dab9 Mon Sep 17 00:00:00 2001 From: sirily11 <32106111+sirily11@users.noreply.github.com> Date: Tue, 19 Jul 2022 02:21:47 +0800 Subject: [PATCH] feat: add jwt support (#9) * feat: add jwt support for services * feat: add jwt token support --- functions/authentication/jest.config.js | 1 + functions/authentication/package.json | 5 +- functions/authentication/pnpm-lock.yaml | 6 ++ functions/authentication/setup.ts | 6 ++ functions/authentication/src/index.test.ts | 13 ++-- functions/authentication/src/index.ts | 24 +++++-- packages/common/Package.resolved | 18 ++++++ packages/common/Package.swift | 2 + .../common/Constants/Environments.swift | 4 ++ .../common/Middlewares/JWTMiddleWare.swift | 31 +++++++++ .../Sources/common/Types/JWTPayload.swift | 32 ++++++++++ .../common/Utils/InitializeHealper.swift | 43 ------------- .../common/Utils/InitializeHelper.swift | 64 +++++++++++++++++++ .../video_transcode_service/Package.resolved | 18 ++++++ .../Sources/App/configure.swift | 11 ++-- .../Tests/AppTests/AppTests.swift | 35 ++++++++-- .../video_upload_service/Package.resolved | 18 ++++++ .../video/video_upload_service/Package.swift | 2 + .../App/Controllers/UploadController.swift | 1 + .../Sources/App/configure.swift | 14 ++-- .../Tests/AppTests/AppTests.swift | 35 ++++++++-- 21 files changed, 308 insertions(+), 75 deletions(-) create mode 100644 functions/authentication/setup.ts create mode 100644 packages/common/Sources/common/Middlewares/JWTMiddleWare.swift create mode 100644 packages/common/Sources/common/Types/JWTPayload.swift delete mode 100644 packages/common/Sources/common/Utils/InitializeHealper.swift create mode 100644 packages/common/Sources/common/Utils/InitializeHelper.swift diff --git a/functions/authentication/jest.config.js b/functions/authentication/jest.config.js index 8cbf894..19e0819 100644 --- a/functions/authentication/jest.config.js +++ b/functions/authentication/jest.config.js @@ -2,4 +2,5 @@ module.exports = { preset: 'ts-jest', testEnvironment: 'node', + setupFilesAfterEnv: ["/setup.ts"], }; \ No newline at end of file diff --git a/functions/authentication/package.json b/functions/authentication/package.json index a10a154..f94eb07 100644 --- a/functions/authentication/package.json +++ b/functions/authentication/package.json @@ -6,8 +6,8 @@ "@types/jest": "^28.1.6", "jest": "^28.1.3", "ts-jest": "^28.0.7", - "wrangler": "2.0.22", - "typescript": "4.7.4" + "typescript": "4.7.4", + "wrangler": "2.0.22" }, "private": true, "scripts": { @@ -16,6 +16,7 @@ "test": "jest" }, "dependencies": { + "@tsndr/cloudflare-worker-jwt": "^2.0.1", "ethers": "^5.6.9" } } diff --git a/functions/authentication/pnpm-lock.yaml b/functions/authentication/pnpm-lock.yaml index 5ea5272..893ed01 100644 --- a/functions/authentication/pnpm-lock.yaml +++ b/functions/authentication/pnpm-lock.yaml @@ -2,6 +2,7 @@ lockfileVersion: 5.4 specifiers: '@cloudflare/workers-types': 3.14.1 + '@tsndr/cloudflare-worker-jwt': ^2.0.1 '@types/jest': ^28.1.6 ethers: ^5.6.9 jest: ^28.1.3 @@ -10,6 +11,7 @@ specifiers: wrangler: 2.0.22 dependencies: + '@tsndr/cloudflare-worker-jwt': 2.0.1 ethers: 5.6.9 devDependencies: @@ -1128,6 +1130,10 @@ packages: '@sinonjs/commons': 1.8.3 dev: true + /@tsndr/cloudflare-worker-jwt/2.0.1: + resolution: {integrity: sha512-nzZgRvO3Yr9wB+0cjcbDoiukDfg9il11gn1PQWtzhHSj6fG0mH0+L9PKBlvGdhichIJnZWoJJ50F03+dI01UJw==} + dev: false + /@types/babel__core/7.1.19: resolution: {integrity: sha512-WEOTgRsbYkvA/KCsDwVEGkd7WAr1e3g31VHQ8zy5gul/V1qKullU/BU5I68X5v7V3GnB9eotmom4v5a5gjxorw==} dependencies: diff --git a/functions/authentication/setup.ts b/functions/authentication/setup.ts new file mode 100644 index 0000000..aeed0aa --- /dev/null +++ b/functions/authentication/setup.ts @@ -0,0 +1,6 @@ +import * as crypto from "crypto"; + +//@ts-ignore +global.crypto = { + subtle: crypto.webcrypto.subtle, +}; diff --git a/functions/authentication/src/index.test.ts b/functions/authentication/src/index.test.ts index d332c59..e0221f3 100644 --- a/functions/authentication/src/index.test.ts +++ b/functions/authentication/src/index.test.ts @@ -2,13 +2,17 @@ import { handler, Message } from "."; import { ethers } from "ethers"; describe("Given a handler", () => { - test("When authentication failed", () => { + beforeAll(() => { + process.env.PASSWORD = "password"; + }); + + test("When authentication failed", async () => { const message: Message = { message: "Hello World", signature: "0x", address: "0x0000000000000000000000000000000000000000", }; - const response = handler(message); + const response = await handler(message); expect(response.statusCode).toBe(403); }); @@ -22,8 +26,9 @@ describe("Given a handler", () => { signature: signature, address: wallet.address, }; - const response = handler(message); + const response = await handler(message); expect(response.statusCode).toBe(200); + let body = JSON.parse(response.body); }); test("When authentication failed", async () => { @@ -36,7 +41,7 @@ describe("Given a handler", () => { signature: signature, address: wallet.address, }; - const response = handler(message); + const response = await handler(message); expect(response.statusCode).toBe(403); }); }); diff --git a/functions/authentication/src/index.ts b/functions/authentication/src/index.ts index 9d551d0..e7005a5 100644 --- a/functions/authentication/src/index.ts +++ b/functions/authentication/src/index.ts @@ -1,4 +1,5 @@ import { ethers } from "ethers"; +import jwt from "@tsndr/cloudflare-worker-jwt"; export interface Message { message: string; @@ -6,25 +7,33 @@ export interface Message { address: string; } +interface JWTPayload { + userId: string; +} + interface HandlerResponse { statusCode: number; body: string; } -export function handler({ +export async function handler({ message, signature, address, -}: Message): HandlerResponse { +}: Message): Promise { try { const signedAddress = ethers.utils.verifyMessage(message, signature); if (address === signedAddress) { + let password = process.env.PASSWORD!; + const payload: JWTPayload = { + userId: address, + }; + let token = await jwt.sign(payload, password); return { statusCode: 200, body: JSON.stringify({ message: "OK", - address: address, - signature: signature, + accessToken: token, }), }; } @@ -34,8 +43,6 @@ export function handler({ statusCode: 403, body: JSON.stringify({ message: "Forbidden", - address: address, - signature: signature, }), }; } @@ -43,10 +50,13 @@ export function handler({ export default { async fetch(request: Request): Promise { const message = await request.json(); - const response = handler(message); + const response = await handler(message); return new Response(response.body, { status: response.statusCode, + headers: { + "Content-Type": "application/json", + }, }); }, }; diff --git a/packages/common/Package.resolved b/packages/common/Package.resolved index 8900666..7f97845 100644 --- a/packages/common/Package.resolved +++ b/packages/common/Package.resolved @@ -72,6 +72,24 @@ "version" : "1.0.2" } }, + { + "identity" : "jwt", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/jwt.git", + "state" : { + "revision" : "506d238a707c0e7c1d2cf690863902eaf3bc4e94", + "version" : "4.2.1" + } + }, + { + "identity" : "jwt-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/jwt-kit.git", + "state" : { + "revision" : "aa7d9cd805b10c1ca17dfc53062ffbc695469cf4", + "version" : "4.6.1" + } + }, { "identity" : "mongokitten", "kind" : "remoteSourceControl", diff --git a/packages/common/Package.swift b/packages/common/Package.swift index df6e484..03722c4 100644 --- a/packages/common/Package.swift +++ b/packages/common/Package.swift @@ -22,6 +22,7 @@ let package = Package( .package(url: "https://github.com/soto-project/soto", from: "6.0.0"), .package(url: "https://github.com/swift-server-community/mqtt-nio", from: "2.6.0"), .package(url: "https://github.com/vapor/fluent-mongo-driver.git", from: "1.0.0"), + .package(url: "https://github.com/vapor/jwt.git", from: "4.0.0"), ], targets: [ .target( @@ -32,6 +33,7 @@ let package = Package( .product(name: "MQTTNIO", package: "mqtt-nio"), .product(name: "SotoS3", package: "soto"), .product(name: "FluentMongoDriver", package: "fluent-mongo-driver"), + .product(name: "JWT", package: "jwt") ] ), .testTarget( diff --git a/packages/common/Sources/common/Constants/Environments.swift b/packages/common/Sources/common/Constants/Environments.swift index b2d904f..bddec1b 100644 --- a/packages/common/Sources/common/Constants/Environments.swift +++ b/packages/common/Sources/common/Constants/Environments.swift @@ -11,3 +11,7 @@ public let ENVIRONMENT_S3_ENDPOINT = "ENDPOINT" public let ENVIRONMENT_S3_REGION = "REGION" public let ENVIRONMENT_S3_BUCKET_NAME = "BUCKET_NAME" public let ENVIRONMENT_TRANSCODING_URL = "TRANSCODING_URL" +/** + JWT Password + */ +public let ENVIRONMENT_PASSWORD = "PASSWORD" diff --git a/packages/common/Sources/common/Middlewares/JWTMiddleWare.swift b/packages/common/Sources/common/Middlewares/JWTMiddleWare.swift new file mode 100644 index 0000000..3f6c3dd --- /dev/null +++ b/packages/common/Sources/common/Middlewares/JWTMiddleWare.swift @@ -0,0 +1,31 @@ +// +// File.swift +// +// +// Created by Qiwei Li on 7/18/22. +// + +import Foundation +import Vapor +import JWT +import JWTKit + + +public class JWTMiddleWare: AsyncMiddleware { + public init(){ + + } + + public func respond(to request: Request, chainingTo next: AsyncResponder) async throws -> Response { + guard let token = request.headers.bearerAuthorization else { + throw Abort(.unauthorized, reason: "No access token found in header") + } + do { + try request.jwt.verify(token.token, as: JWTVerificationPayload.self) + + } catch let error as JWTError { + throw Abort(.unauthorized, reason: error.reason) + } + return try await next.respond(to: request) + } +} diff --git a/packages/common/Sources/common/Types/JWTPayload.swift b/packages/common/Sources/common/Types/JWTPayload.swift new file mode 100644 index 0000000..287e00e --- /dev/null +++ b/packages/common/Sources/common/Types/JWTPayload.swift @@ -0,0 +1,32 @@ +// +// File.swift +// +// +// Created by Qiwei Li on 7/18/22. +// + +import Foundation +import JWT + +public struct JWTVerificationPayload: JWTPayload { + // Maps the longer Swift property names to the + // shortened keys used in the JWT payload. + enum CodingKeys: String, CodingKey { + case subject = "sub" + case expiration = "exp" + case userId = "userId" + } + + // The "sub" (subject) claim identifies the principal that is the + // subject of the JWT. + var subject: SubjectClaim + + var expiration: ExpirationClaim + + // UserId + var userId: String + + public func verify(using signer: JWTSigner) throws { + try self.expiration.verifyNotExpired() + } +} diff --git a/packages/common/Sources/common/Utils/InitializeHealper.swift b/packages/common/Sources/common/Utils/InitializeHealper.swift deleted file mode 100644 index 262ec51..0000000 --- a/packages/common/Sources/common/Utils/InitializeHealper.swift +++ /dev/null @@ -1,43 +0,0 @@ -import Vapor -import Fluent -import SotoS3 -import FluentMongoDriver -import MQTTNIO - -// configures your application -public func initializeDB(_ app: Application) throws { - try app.databases.use(.mongo( - connectionString: Environment.get("DATABASE_URL")! - ), as: .mongo) -} - -public func initializeAWS(_ app: Application) { - let accessKey = Environment.get(ENVIRONMENT_ACCESS_KEY)! - let secretKey = Environment.get(ENVIRONMENT_SECRET_KEY)! - let endpoint = Environment.get(ENVIRONMENT_S3_ENDPOINT)! - let region = Environment.get(ENVIRONMENT_S3_REGION)! - - var configuration = HTTPClient.Configuration() - configuration.httpVersion = .http1Only - - app.aws.client = AWSClient( - credentialProvider: .static(accessKeyId: accessKey, secretAccessKey: secretKey), - httpClientProvider: .shared(HTTPClient( - eventLoopGroupProvider: .createNew, - configuration: configuration - )) - ) - app.aws.s3 = S3(client: app.aws.client, region: .other(region), endpoint: endpoint) -} - -public func initializeMQTT(_ app: Application) throws { - let client = MQTTClient( - host: "localhost", - port: 1883, - identifier: "My Client", - eventLoopGroupProvider: .createNew, - configuration: .init(userName: "user", password: "password") - ) - _ = try client.connect().wait() - app.mqtt.client = client -} diff --git a/packages/common/Sources/common/Utils/InitializeHelper.swift b/packages/common/Sources/common/Utils/InitializeHelper.swift new file mode 100644 index 0000000..bc9555f --- /dev/null +++ b/packages/common/Sources/common/Utils/InitializeHelper.swift @@ -0,0 +1,64 @@ +import Vapor +import Fluent +import SotoS3 +import FluentMongoDriver +import MQTTNIO +import JWT + + +public class InitializeHelper { + let app: Application + + public init(app: Application) { + self.app = app + } + + // configures your application + public func initializeDB() throws { + try app.databases.use(.mongo( + connectionString: Environment.get(ENVIRONMENT_DB_KEY)! + ), as: .mongo) + } + + public func initializeAWS() throws { + let accessKey = Environment.get(ENVIRONMENT_ACCESS_KEY)! + let secretKey = Environment.get(ENVIRONMENT_SECRET_KEY)! + let endpoint = Environment.get(ENVIRONMENT_S3_ENDPOINT)! + let region = Environment.get(ENVIRONMENT_S3_REGION)! + + var configuration = HTTPClient.Configuration() + configuration.httpVersion = .http1Only + + app.aws.client = AWSClient( + credentialProvider: .static(accessKeyId: accessKey, secretAccessKey: secretKey), + httpClientProvider: .shared(HTTPClient( + eventLoopGroupProvider: .createNew, + configuration: configuration + )) + ) + app.aws.s3 = S3(client: app.aws.client, region: .other(region), endpoint: endpoint) + } + + public func initializeMQTT() throws { + let client = MQTTClient( + host: "localhost", + port: 1883, + identifier: "Client", + eventLoopGroupProvider: .createNew, + configuration: .init(userName: "user", password: "password") + ) + _ = try client.connect().wait() + app.mqtt.client = client + } + + public func initializeJWT() throws { + app.jwt.signers.use(.hs256(key: Environment.get(ENVIRONMENT_PASSWORD)!)) + } + +} + + + + + + diff --git a/services/video/video_transcode_service/Package.resolved b/services/video/video_transcode_service/Package.resolved index 8e2b2f3..909de8c 100644 --- a/services/video/video_transcode_service/Package.resolved +++ b/services/video/video_transcode_service/Package.resolved @@ -81,6 +81,24 @@ "version" : "1.0.2" } }, + { + "identity" : "jwt", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/jwt.git", + "state" : { + "revision" : "506d238a707c0e7c1d2cf690863902eaf3bc4e94", + "version" : "4.2.1" + } + }, + { + "identity" : "jwt-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/jwt-kit.git", + "state" : { + "revision" : "aa7d9cd805b10c1ca17dfc53062ffbc695469cf4", + "version" : "4.6.1" + } + }, { "identity" : "mongokitten", "kind" : "remoteSourceControl", diff --git a/services/video/video_transcode_service/Sources/App/configure.swift b/services/video/video_transcode_service/Sources/App/configure.swift index b98911c..3b8105f 100644 --- a/services/video/video_transcode_service/Sources/App/configure.swift +++ b/services/video/video_transcode_service/Sources/App/configure.swift @@ -21,6 +21,7 @@ fileprivate func checkENV(_ app: Application) throws { // configures your application public func configure(_ app: Application) throws { + let initializeHelper = InitializeHelper(app: app) let corsConfiguration = CORSMiddleware.Configuration( allowedOrigin: .all, allowedMethods: [.GET, .POST, .PUT, .OPTIONS, .DELETE, .PATCH], allowedHeaders: [.accept, .authorization, .contentType, .origin, .xRequestedWith, .userAgent, .accessControlAllowOrigin] @@ -29,10 +30,12 @@ public func configure(_ app: Application) throws { app.middleware.use(cors, at: .beginning) try checkENV(app) - try initializeMQTT(app) - try initializeDB(app) - initializeAWS(app) - + try initializeHelper.initializeDB() + try initializeHelper.initializeMQTT() + try initializeHelper.initializeAWS() + try initializeHelper.initializeJWT() + + // register routes try routes(app) } diff --git a/services/video/video_transcode_service/Tests/AppTests/AppTests.swift b/services/video/video_transcode_service/Tests/AppTests/AppTests.swift index 16345d2..fd63847 100644 --- a/services/video/video_transcode_service/Tests/AppTests/AppTests.swift +++ b/services/video/video_transcode_service/Tests/AppTests/AppTests.swift @@ -4,6 +4,8 @@ import XCTVapor @testable import model final class AppTests: XCTestCase { + let payload = JWTVerificationPayload(subject: .init(value: "Hello"), expiration: .init(value: .now.addingTimeInterval(10)), userId: "0xabcde") + override func setUp() async throws { setenv(ENVIRONMENT_DB_KEY, "mongodb://user:password@localhost:27017/video", 1) setenv(ENVIRONMENT_S3_REGION, "sgp1", 1) @@ -11,6 +13,7 @@ final class AppTests: XCTestCase { setenv(ENVIRONMENT_S3_BUCKET_NAME, "video-trading", 1) setenv(ENVIRONMENT_ACCESS_KEY, "test", 1) setenv(ENVIRONMENT_SECRET_KEY, "test", 1) + setenv(ENVIRONMENT_PASSWORD, "password", 1) } func cleanUp(app: Application) { @@ -24,6 +27,7 @@ final class AppTests: XCTestCase { app.shutdown() } try configure(app) + let token = try app.jwt.signers.sign(payload) let video = VideoInfo(title: "a", labels: [], description: nil, cover: nil, source: "http://localhost:4566/video-trading/upload/file.mov", transcoding: [], status: .uploaded, statusDescription: nil, length: nil, @@ -35,7 +39,9 @@ final class AppTests: XCTestCase { try await video.create(on: app.db) - try app.test(.GET, "video/transcoding/result/\(video.id!)", afterResponse: { res in + try app.test(.GET, "video/transcoding/result/\(video.id!)", beforeRequest: { req in + req.headers.bearerAuthorization = BearerAuthorization(token: token) + }, afterResponse: { res in let content = try res.content.decode([TranscodingJob].self) XCTAssertEqual(res.status, .ok) XCTAssertEqual(content.count, 0) @@ -43,6 +49,7 @@ final class AppTests: XCTestCase { try app.test(.POST, "video/transcoding/analyzing", beforeRequest: { req in try req.content.encode(AnalyzingRequest(videoId: video.id!, quality: .resolution360P, length: 10, cover: "https://my_cover.png", fileName: video.fileName)) + req.headers.bearerAuthorization = BearerAuthorization(token: token) }, afterResponse: { res in XCTAssertEqual(res.status, .ok) }) @@ -54,7 +61,9 @@ final class AppTests: XCTestCase { - try app.test(.GET, "video/transcoding/result/\(video.id!)", afterResponse: { res in + try app.test(.GET, "video/transcoding/result/\(video.id!)", beforeRequest: { req in + req.headers.bearerAuthorization = BearerAuthorization(token: token) + }, afterResponse: { res in XCTAssertEqual(res.status, .ok) let content = try res.content.decode([TranscodingJob].self) XCTAssertEqual(content.count, 3) @@ -70,6 +79,7 @@ final class AppTests: XCTestCase { try app.test(.POST, "video/transcoding/result", beforeRequest: { req in videoTranscodingInfo.status = .uploaded try req.content.encode(videoTranscodingInfo) + req.headers.bearerAuthorization = BearerAuthorization(token: token) }, afterResponse: { res in let content = try res.content.decode(TranscodingInfo.self) XCTAssertEqual(res.status, .ok) @@ -84,6 +94,7 @@ final class AppTests: XCTestCase { app.shutdown() } try configure(app) + let token = try app.jwt.signers.sign(payload) let video = VideoInfo(title: "a", labels: [], description: nil, cover: nil, source: "http://localhost:4566/video-trading/upload/file2.mov", transcoding: [], status: .uploaded, statusDescription: nil, length: nil, @@ -95,7 +106,9 @@ final class AppTests: XCTestCase { try await video.create(on: app.db) - try app.test(.GET, "video/transcoding/result/\(video.id!)", afterResponse: { res in + try app.test(.GET, "video/transcoding/result/\(video.id!)",beforeRequest: { req in + req.headers.bearerAuthorization = BearerAuthorization(token: token) + } ,afterResponse: { res in let content = try res.content.decode([TranscodingJob].self) XCTAssertEqual(res.status, .ok) XCTAssertEqual(content.count, 0) @@ -103,11 +116,14 @@ final class AppTests: XCTestCase { try app.test(.POST, "video/transcoding/analyzing", beforeRequest: { req in try req.content.encode(AnalyzingRequest(videoId: video.id!, quality: .resolution360P, length: 10, cover: "", fileName: video.fileName)) + req.headers.bearerAuthorization = BearerAuthorization(token: token) }, afterResponse: { res in XCTAssertEqual(res.status, .ok) }) - try app.test(.GET, "video/transcoding/result/\(video.id!)", afterResponse: { res in + try app.test(.GET, "video/transcoding/result/\(video.id!)", beforeRequest: { req in + req.headers.bearerAuthorization = BearerAuthorization(token: token) + } , afterResponse: { res in XCTAssertEqual(res.status, .ok) let content = try res.content.decode([TranscodingJob].self) XCTAssertEqual(content.count, 3) @@ -122,6 +138,7 @@ final class AppTests: XCTestCase { try app.test(.POST, "video/transcoding/result", beforeRequest: { req in videoTranscodingInfo.status = .uploaded try req.content.encode(videoTranscodingInfo) + req.headers.bearerAuthorization = BearerAuthorization(token: token) }, afterResponse: { res in let content = try res.content.decode(TranscodingInfo.self) XCTAssertEqual(res.status, .ok) @@ -137,6 +154,7 @@ final class AppTests: XCTestCase { app.shutdown() } try configure(app) + let token = try app.jwt.signers.sign(payload) let video = VideoInfo(title: "a", labels: [], description: nil, cover: nil, source: "http://localhost:4566/video-trading/upload/file3.mov", transcoding: [], status: .uploaded, statusDescription: nil, length: nil, @@ -147,7 +165,9 @@ final class AppTests: XCTestCase { try await video.create(on: app.db) - try app.test(.POST, "video/transcoding/\(video.id!.uuidString)", afterResponse: { res in + try app.test(.POST, "video/transcoding/\(video.id!.uuidString)", beforeRequest: { req in + req.headers.bearerAuthorization = BearerAuthorization(token: token) + }, afterResponse: { res in XCTAssertEqual(res.status, .ok) let data = try res.content.decode(AnalyzingJob.self) XCTAssertContains(data.cover, "http://localhost:4566/video-trading/cover/\(video.id!.uuidString).png") @@ -165,6 +185,7 @@ final class AppTests: XCTestCase { app.shutdown() } try configure(app) + let token = try app.jwt.signers.sign(payload) let video = VideoInfo(title: "a", labels: [], description: nil, cover: nil, source: "http://localhost:4566/video-trading/upload/file4.mov", transcoding: [], status: .uploaded, statusDescription: nil, length: nil, @@ -175,7 +196,9 @@ final class AppTests: XCTestCase { try await video.create(on: app.db) - try app.test(.POST, "video/transcoding/\(UUID().uuidString)", afterResponse: { res in + try app.test(.POST, "video/transcoding/\(UUID().uuidString)",beforeRequest: { req in + req.headers.bearerAuthorization = BearerAuthorization(token: token) + } , afterResponse: { res in XCTAssertEqual(res.status, .notFound) }) } diff --git a/services/video/video_upload_service/Package.resolved b/services/video/video_upload_service/Package.resolved index fac28ad..a7ea006 100644 --- a/services/video/video_upload_service/Package.resolved +++ b/services/video/video_upload_service/Package.resolved @@ -90,6 +90,24 @@ "version" : "1.0.2" } }, + { + "identity" : "jwt", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/jwt.git", + "state" : { + "revision" : "506d238a707c0e7c1d2cf690863902eaf3bc4e94", + "version" : "4.2.1" + } + }, + { + "identity" : "jwt-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/jwt-kit.git", + "state" : { + "revision" : "aa7d9cd805b10c1ca17dfc53062ffbc695469cf4", + "version" : "4.6.1" + } + }, { "identity" : "mongokitten", "kind" : "remoteSourceControl", diff --git a/services/video/video_upload_service/Package.swift b/services/video/video_upload_service/Package.swift index 75d2f18..fc02716 100644 --- a/services/video/video_upload_service/Package.swift +++ b/services/video/video_upload_service/Package.swift @@ -14,6 +14,7 @@ let package = Package( .package(url: "https://github.com/soto-project/soto", from: "6.0.0"), .package(url: "https://github.com/sirily11/env-checker", from: "1.0.0" ), .package(url: "https://github.com/swift-server-community/mqtt-nio", from: "2.6.0"), + .package(url: "https://github.com/vapor/jwt.git", from: "4.0.0"), .package(path: "../../../packages/model"), .package(path: "../../../packages/common"), .package(path: "../../../packages/client"), @@ -26,6 +27,7 @@ let package = Package( .product(name: "FluentMongoDriver", package: "fluent-mongo-driver"), .product(name: "MQTTNIO", package: "mqtt-nio"), .product(name: "Vapor", package: "vapor"), + .product(name: "JWT", package: "jwt"), .product(name: "SotoS3", package: "soto"), .product(name: "model", package: "model"), .product(name: "common", package: "common"), diff --git a/services/video/video_upload_service/Sources/App/Controllers/UploadController.swift b/services/video/video_upload_service/Sources/App/Controllers/UploadController.swift index 033edb9..f118f5d 100644 --- a/services/video/video_upload_service/Sources/App/Controllers/UploadController.swift +++ b/services/video/video_upload_service/Sources/App/Controllers/UploadController.swift @@ -15,6 +15,7 @@ struct UploadController: RouteCollection { } func delete(req: Request) async throws -> Response { + let previousInfo = try await findPreviousInfoById(req: req) return try await req.delete(video: previousInfo) } diff --git a/services/video/video_upload_service/Sources/App/configure.swift b/services/video/video_upload_service/Sources/App/configure.swift index a8ad986..e1c18af 100644 --- a/services/video/video_upload_service/Sources/App/configure.swift +++ b/services/video/video_upload_service/Sources/App/configure.swift @@ -6,13 +6,14 @@ import common import env import MQTTNIO import client +import JWT fileprivate func checkENV(_ app: Application) throws { let checker = EnvChecker(envs: [ ENVIRONMENT_DB_KEY, ENVIRONMENT_S3_ENDPOINT, ENVIRONMENT_S3_REGION ,ENVIRONMENT_ACCESS_KEY, - ENVIRONMENT_SECRET_KEY + ENVIRONMENT_SECRET_KEY, ENVIRONMENT_PASSWORD, ]) let checkResult = checker.check() @@ -30,12 +31,15 @@ public func configure(_ app: Application) throws { ) let cors = CORSMiddleware(configuration: corsConfiguration) app.middleware.use(cors, at: .beginning) + app.middleware.use(JWTMiddleWare()) - try checkENV(app) - try initializeDB(app) - initializeAWS(app) - try initializeMQTT(app) + let initializeHelper = InitializeHelper(app: app) + try checkENV(app) + try initializeHelper.initializeDB() + try initializeHelper.initializeAWS() + try initializeHelper.initializeJWT() + app.videoTranscoding.client = VideoTranscodingClient() // register routes diff --git a/services/video/video_upload_service/Tests/AppTests/AppTests.swift b/services/video/video_upload_service/Tests/AppTests/AppTests.swift index e0ac7bd..1f9c6ba 100644 --- a/services/video/video_upload_service/Tests/AppTests/AppTests.swift +++ b/services/video/video_upload_service/Tests/AppTests/AppTests.swift @@ -4,6 +4,8 @@ import XCTVapor final class AppTests: XCTestCase { + let payload = JWTVerificationPayload(subject: .init(value: "Hello"), expiration: .init(value: .now.addingTimeInterval(10)), userId: "0xabcde") + override func setUp() async throws { setenv(ENVIRONMENT_DB_KEY, "mongodb://user:password@localhost:27017/video", 1) setenv(ENVIRONMENT_S3_REGION, "sgp1", 1) @@ -12,6 +14,7 @@ final class AppTests: XCTestCase { setenv(ENVIRONMENT_ACCESS_KEY, "test", 1) setenv(ENVIRONMENT_SECRET_KEY, "test", 1) setenv(ENVIRONMENT_TRANSCODING_URL, "https://video.mock-api-services.workers.dev", 1) + setenv(ENVIRONMENT_PASSWORD, "password", 1) } func testUpload() throws { @@ -20,10 +23,13 @@ final class AppTests: XCTestCase { app.shutdown() } try configure(app) + let token = try app.jwt.signers.sign(payload) var videoId: String = "" try app.test(.POST, "video/upload", beforeRequest: { req in try req.content.encode(VideoInfoRequest(title: "Hello world", labels: [], fileName: "test.mov")) + req.headers.bearerAuthorization = BearerAuthorization(token: token) + }, afterResponse: { res in let data = try res.content.decode(UploadResponse.self) XCTAssertEqual(res.status, .ok) @@ -32,18 +38,23 @@ final class AppTests: XCTestCase { videoId = data.id.uuidString }) - try app.test(.GET, "video/status/\(videoId)") { statusResult in + try app.test(.GET, "video/status/\(videoId)", beforeRequest: { req in + req.headers.bearerAuthorization = BearerAuthorization(token: token) + }) { statusResult in let status = try statusResult.content.decode(VideoInfo.self) XCTAssertEqual(status.status, .pending) } try app.test(.PATCH, "video/status/\(videoId)", beforeRequest: { req in try req.content.encode(UpdateStatusRequest(status: .success)) + req.headers.bearerAuthorization = BearerAuthorization(token: token) }) { res in XCTAssertEqual(res.status, .accepted) } - try app.test(.GET, "video/status/\(videoId)") { statusResult in + try app.test(.GET, "video/status/\(videoId)", beforeRequest: { req in + req.headers.bearerAuthorization = BearerAuthorization(token: token) + }) { statusResult in let status = try statusResult.content.decode(VideoInfo.self) XCTAssertEqual(status.status, .failed) } @@ -56,11 +67,13 @@ final class AppTests: XCTestCase { app.shutdown() } try configure(app) - + let token = try app.jwt.signers.sign(payload) let info = VideoInfo(title: "Mock", labels: [], description: nil, cover: nil, source: "https://localhost:4566/video-trading/test.mov", transcoding: [], status: .uploaded, statusDescription: nil, length: nil, fileName: "test.mov", bucketName: "test-bucket") try await info.create(on: app.db) - try app.test(.GET, "video/download/\(info.id!.uuidString)") { res in + try app.test(.GET, "video/download/\(info.id!.uuidString)", beforeRequest: { req in + req.headers.bearerAuthorization = BearerAuthorization(token: token) + }) { res in XCTAssertEqual(res.status, .ok) let downloadResponse = try res.content.decode(DownloadResponse.self) @@ -68,4 +81,18 @@ final class AppTests: XCTestCase { XCTAssertEqual(downloadResponse.id.uuidString, info.id?.uuidString) } } + + func testUnauthorized() async throws { + let app = Application(.testing) + defer { + app.shutdown() + } + try configure(app) + let info = VideoInfo(title: "Mock", labels: [], description: nil, cover: nil, source: "https://localhost:4566/video-trading/test.mov", transcoding: [], status: .uploaded, statusDescription: nil, length: nil, fileName: "test.mov", bucketName: "test-bucket") + try await info.create(on: app.db) + + try app.test(.GET, "video/download/\(info.id!.uuidString)") { res in + XCTAssertEqual(res.status, .unauthorized) + } + } }