diff --git a/Package.resolved b/Package.resolved index 6f57d35..7971144 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "d53dfe2770a8b5d4063e71737e72637f59cec754bf4553178a66d10f39bfb1ee", + "originHash" : "01eb28d6d5d4bfc205f7c8b2f8a3dc3a3d278b26c75f239cbd1cf26e4482e33d", "pins" : [ { "identity" : "async-http-client", @@ -55,6 +55,24 @@ "version" : "4.8.1" } }, + { + "identity" : "jwt", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/jwt.git", + "state" : { + "revision" : "af1c59762d70d1065ddbc0d7902ea9b3dacd1a26", + "version" : "5.1.2" + } + }, + { + "identity" : "jwt-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/jwt-kit.git", + "state" : { + "revision" : "03f5013f0b547ce43abe45e7e90711303a3e5495", + "version" : "5.1.2" + } + }, { "identity" : "multipart-kit", "kind" : "remoteSourceControl", @@ -127,6 +145,15 @@ "version" : "1.2.0" } }, + { + "identity" : "swift-certificates", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-certificates.git", + "state" : { + "revision" : "999fd70c7803da89f3904d635a6815a2a7cd7585", + "version" : "1.10.0" + } + }, { "identity" : "swift-collections", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index 09e2558..258a04f 100644 --- a/Package.swift +++ b/Package.swift @@ -15,6 +15,8 @@ let package = Package( .package(url: "https://github.com/vapor/fluent-sqlite-driver.git", from: "4.6.0"), // 🔵 Non-blocking, event-driven networking for Swift. Used for custom executors .package(url: "https://github.com/apple/swift-nio.git", from: "2.65.0"), + // JWTs + .package(url: "https://github.com/vapor/jwt.git", from: "5.0.0"), ], targets: [ .executableTarget( @@ -25,6 +27,7 @@ let package = Package( .product(name: "Vapor", package: "vapor"), .product(name: "NIOCore", package: "swift-nio"), .product(name: "NIOPosix", package: "swift-nio"), + .product(name: "JWT", package: "jwt"), ], swiftSettings: swiftSettings ), diff --git a/Sources/stella/Controllers/OpenApi/AuthController.swift b/Sources/stella/Controllers/OpenApi/AuthController.swift index 8be0eae..26b3c9e 100644 --- a/Sources/stella/Controllers/OpenApi/AuthController.swift +++ b/Sources/stella/Controllers/OpenApi/AuthController.swift @@ -74,7 +74,36 @@ struct AuthController: RouteCollection { guard let account = account else { return Response(status: .badRequest, body: "{\"error\": \"Bad Request\", \"message\": \"Invalid playerId provided.\"}") } - return Response(status: .notImplemented) + + let zatExpiry = Date.now.advanced(by: 43200) + let zrtExpiry = Date.now.advanced(by: 2592000) + + let zatTokenJWT = generateToken(accountId: try account.requireID(), expires: zatExpiry, type: .ZAT) + let zrtTokenJWT = generateToken(accountId: try account.requireID(), expires: zrtExpiry, type: .ZRT) + + let zatToken = try await req.jwt.sign(zatTokenJWT) + let zrtToken = try await req.jwt.sign(zrtTokenJWT) + + let res = LoginDeviceRes( + zatExpiryTime: Int(zatExpiry.timeIntervalSince1970) * 1000, + zrtExpiryTime: Int(zrtExpiry.timeIntervalSince1970) * 1000, + firstLogin: true, + externalToken: "", + zat: zatToken, + zrt: zrtToken, + player: Player( + idpId: account.idpId, + appId: account.appId, + playerId: String(try account.requireID()), + pushOption: PushOptionResponse(night: "n", player: "n"), + regTime: Int(account.regDate.timeIntervalSince1970), + idpAlias: idpAlias, + firstLoginTime: Int(account.firstLogin.timeIntervalSince1970), + status: account.status + ) + ) + + return try await res.encodeResponse(for: req) } } @@ -134,11 +163,8 @@ struct LoginDeviceRes: Content { struct Player: Content { let idpId: String let appId: String - let lang: String let playerId: String - let agreement: AgreementResponse let pushOption: PushOptionResponse - let lastLoginTime: Int let regTime: Int let idpAlias: String let firstLoginTime: Int diff --git a/Sources/stella/Models/Account.swift b/Sources/stella/Models/Account.swift index 86cbb8c..1f2b752 100644 --- a/Sources/stella/Models/Account.swift +++ b/Sources/stella/Models/Account.swift @@ -1,5 +1,4 @@ import Fluent -import struct Foundation.UUID /// Property wrappers interact poorly with `Sendable` checking, causing a warning for the `@ID` property /// It is recommended you write your model with sendability checking on and then suppress the warning diff --git a/Sources/stella/configure.swift b/Sources/stella/configure.swift index 97c1364..3ddd91f 100644 --- a/Sources/stella/configure.swift +++ b/Sources/stella/configure.swift @@ -2,6 +2,7 @@ import NIOSSL import Fluent import FluentSQLiteDriver import Vapor +import JWT // configures your application public func configure(_ app: Application) async throws { @@ -14,6 +15,9 @@ public func configure(_ app: Application) async throws { app.http.server.configuration.hostname = "0.0.0.0" app.http.server.configuration.port = 8000 + // JWT + await app.jwt.keys.add(hmac: "secret", digestAlgorithm: .sha256) + // register routes try routes(app) } diff --git a/Sources/stella/util.swift b/Sources/stella/util.swift index c58f48d..d3c051c 100644 --- a/Sources/stella/util.swift +++ b/Sources/stella/util.swift @@ -1,3 +1,43 @@ +import JWT +import Fluent +import Foundation + func generateIdpAlias(appId: String, deviceId: String, serialNo: String) -> String { return "\(appId):\(deviceId):\(serialNo)" } + +func generateToken(accountId: Int, expires: Date, type: SessionType) -> SessionPayload { + return SessionPayload( + accountId: .init(value: String(accountId)), + expiration: .init(value: expires), + type: type.rawValue + ) +} + +struct SessionPayload: JWTPayload { + enum CodingKeys: String, CodingKey { + case accountId = "sub" + case expiration = "exp" + case type = "type" + } + + // The "sub" (subject) claim identifies the principal that is the + // subject of the JWT. + var accountId: SubjectClaim + + // The "exp" (expiration time) claim identifies the expiration time on + // or after which the JWT MUST NOT be accepted for processing. + var expiration: ExpirationClaim + + // Custom data. + // If true, the user is an admin. + var type: Int + + // Run any additional verification logic beyond + // signature verification here. + // Since we have an ExpirationClaim, we will + // call its verify method. + func verify(using algorithm: some JWTAlgorithm) async throws { + try self.expiration.verifyNotExpired() + } +}