From 057582bdde059f0ac81e75fd7be7facc3aa79672 Mon Sep 17 00:00:00 2001 From: rpm-mcarman <65228808+rpm-mcarman@users.noreply.github.com> Date: Tue, 16 Apr 2024 13:34:49 -0400 Subject: [PATCH] Multiple Auth Methods --- Sources/App/Controllers/cntrlAssociate.swift | 62 +++++++++++++++ Sources/App/Migrations/AllMigrations.swift | 1 + Sources/App/Models/mdlAssociate.swift | 83 ++++++++++++++++---- Sources/App/Models/mdlJWT.swift | 30 ++++++- Sources/App/Models/mdlToken.swift | 41 +++++++++- Sources/App/configure.swift | 10 ++- Sources/App/routes.swift | 8 ++ jwtRS256.key | 51 ++++++++++++ jwtRS256.key.pub | 14 ++++ 9 files changed, 280 insertions(+), 20 deletions(-) create mode 100644 jwtRS256.key create mode 100644 jwtRS256.key.pub diff --git a/Sources/App/Controllers/cntrlAssociate.swift b/Sources/App/Controllers/cntrlAssociate.swift index fa326a8..b33489a 100644 --- a/Sources/App/Controllers/cntrlAssociate.swift +++ b/Sources/App/Controllers/cntrlAssociate.swift @@ -18,9 +18,69 @@ struct cntrlAssociate: RouteCollection { rts.get(":x", use: read) rts.post(use: create) rts.delete(":x", use: delete) + rts.post("create", use: createUser) + let passwordProtected = rts.grouped(mdlAssociate.authenticator(), mdlAssociate.guardMiddleware()) + passwordProtected.post("login", use: login) + passwordProtected.post("tlogin", use: jwtauth) + let tokenProtected = rts.grouped(mdlToken.authenticator()) + tokenProtected.get("tauth", use: auth) + //Additional routs added here } + @Sendable + func jwtauth(req: Request) async throws -> ClientTokenResponse{ + let user = try req.auth.require(mdlAssociate.self) + + let payload = SessionToken(user: user) + return ClientTokenResponse(token: try req.jwt.sign(payload)) + } + + @Sendable + func auth(req: Request) async throws -> Response{ + let result = try req.auth.require(mdlAssociate.self) + let assoc = mdlAssociate.Output(id: result.id, firstname: result.firstname, lastname: result.lastname) + guard let response = try? await assoc.encodeResponse(status: .ok, for: req) else{ + throw Abort(.noContent, reason: "Something went wrong decoding data. Associate could not be shown.") + } + return response + } + + @Sendable + func login(req: Request) async throws -> Response{ + guard let result = try? req.auth.require(mdlAssociate.self) else{ + throw Abort(.unauthorized) + } + let token = try await result.generateToken() + try await token.save(on: req.db) + guard let response = try? await token.encodeResponse(status: .ok, for: req) else{ + throw Abort(.noContent, reason: "Something went wrong decoding data. Token could not be shown.") + } + return response + } + + @Sendable + func createUser(req: Request) async throws -> Response{ + try mdlAssociate.CreateUser.validate(content: req) + let create = try req.content.decode(mdlAssociate.CreateUser.self) + guard create.password == create.confirmpassword else{ + throw Abort(.badRequest, reason: "Passwords did not match") + } + let user = try mdlAssociate( + firstname: create.firstname, + lastname: create.lastname, + username: create.username, + password: Bcrypt.hash(create.password) + ) + try await user.save(on: req.db) + let assoc = mdlAssociate.Output(id: user.id, firstname: user.firstname, lastname: user.lastname) + guard let response = try? await assoc.encodeResponse(status: .ok, for: req) else{ + throw Abort(.noContent, reason: "Something went wrong decoding data. Associate could not be shown.") + } + return response + } + + @Sendable func index(req: Request) async throws -> Response { let obj = try await mdlAssociate.ObjQuery(req: req).all() @@ -66,6 +126,8 @@ struct cntrlAssociate: RouteCollection { } + + @Sendable func delete(req: Request) async throws -> Response { guard let obj = try? await mdlAssociate.ObjQuery(req: req).first() else { diff --git a/Sources/App/Migrations/AllMigrations.swift b/Sources/App/Migrations/AllMigrations.swift index d027d37..f3969ae 100644 --- a/Sources/App/Migrations/AllMigrations.swift +++ b/Sources/App/Migrations/AllMigrations.swift @@ -20,6 +20,7 @@ public func doMigrations(_ app: Application) async throws{ app.migrations.add(mdlXAssocDept.Create()) app.migrations.add(mdlXDocDept.Create()) app.migrations.add(mdlDocument.tmpField()) + app.migrations.add(mdlAssociate.Mod1()) try await app.autoMigrate() } diff --git a/Sources/App/Models/mdlAssociate.swift b/Sources/App/Models/mdlAssociate.swift index afd15e3..d93e3ca 100644 --- a/Sources/App/Models/mdlAssociate.swift +++ b/Sources/App/Models/mdlAssociate.swift @@ -10,15 +10,16 @@ import Fluent import Vapor import Foundation import FluentKit -import JWT final class mdlAssociate: Model, Content { static let schema = "associate" @ID(custom: "id", generatedBy: .database) var id: Int? - @Field(key: "firstName") var firstname: String? - @Field(key: "lastName") var lastname: String? + @Field(key: "firstName") var firstname: String + @Field(key: "lastName") var lastname: String + @Field(key: "userName") var username: String + @Field(key: "password") var password: String @Siblings(through: mdlXAssocDept.self, from: \.$associate, to: \.$department) var departments: [mdlDepartment] @@ -30,34 +31,31 @@ final class mdlAssociate: Model, Content { init() { } init(id: Int? = nil, - firstname: String? = nil, - lastname: String? = nil, + firstname: String, + lastname: String, + username: String, + password: String , createuserid: Int? = nil, updateuserid: Int? = nil ) { self.id = id self.firstname = firstname self.lastname = lastname + self.username = username + self.password = password self.createuserid = createuserid self.updateuserid = updateuserid } } extension mdlAssociate{ - struct User: JWTPayload{ - var id: Int - var email: String - var password: String - - func verify(using signer: JWTSigner) throws{ - - } - } struct Create: AsyncMigration { func prepare(on database: Database) async throws { try await database.schema("associate") .field("id", .int, .identifier(auto: true)) .field("firstName", .string) .field("lastName", .string) +// .field("userName", .string) +// .field("password", .string) .field("createDate", .datetime) .field("createUserId", .int) .field("updateDate", .datetime) @@ -69,6 +67,21 @@ extension mdlAssociate{ try await database.schema("associate").delete() } } + struct Mod1: AsyncMigration{ + func prepare(on database: Database) async throws { + try await database.schema("associate") + .field("userName", .string).unique(on: "userName") + .field("password", .string) + .update() + } + func revert(on database: Database) async throws { + try await database.schema("associate") + .deleteField("userName") + .deleteField("password") + .update() + } + } + public static func ObjQuery(req: Request) async throws -> QueryBuilder{ let query = mdlAssociate.query(on: req.db) @@ -107,4 +120,44 @@ extension mdlAssociate{ } } } - +extension mdlAssociate{ + struct CreateUser: Content{ + var username: String + var password: String + var confirmpassword: String + var firstname: String + var lastname: String + } + struct Output: Content{ + var id: Int? + var firstname: String + var lastname: String + } +} +extension mdlAssociate.CreateUser: Validatable{ + static func validations(_ validations: inout Validations){ + validations.add("firstname", as: String.self, is: !.empty) + validations.add("lastname", as: String.self, is: !.empty) + validations.add("username", as: String.self, is: !.empty) + validations.add("password", as: String.self, is: .count(8...)) + } +} +extension mdlAssociate: ModelAuthenticatable{ + static let usernameKey = \mdlAssociate.$username + + static var passwordHashKey = \mdlAssociate.$password + + func verify(password: String) throws -> Bool { + try Bcrypt.verify(password, created: self.password) + } + + +} +extension mdlAssociate{ + func generateToken() async throws -> mdlToken { + try .init( + value: [UInt8].random(count: 16).base64, + userid: self.requireID() + ) + } +} diff --git a/Sources/App/Models/mdlJWT.swift b/Sources/App/Models/mdlJWT.swift index f9c59c1..ac47e8c 100644 --- a/Sources/App/Models/mdlJWT.swift +++ b/Sources/App/Models/mdlJWT.swift @@ -5,4 +5,32 @@ // Created by Michiel Carman on 4/16/24. // -import Foundation +import Vapor +import Fluent +import JWT + +struct SessionToken: Content, Authenticatable, JWTPayload{ + + var expirationtime: TimeInterval = 60 * 15 + var expiration: ExpirationClaim + var userid: Int + + init(userid: Int){ + self.userid = userid + self.expiration = ExpirationClaim(value: Date().addingTimeInterval(expirationtime)) + } + + init(user: mdlAssociate){ + self.userid = try! user.requireID() + self.expiration = ExpirationClaim(value: Date().addingTimeInterval(expirationtime)) + } + + func verify(using signer: JWTKit.JWTSigner) throws { + try expiration.verifyNotExpired() + } + + +} +struct ClientTokenResponse: Content{ + var token: String +} diff --git a/Sources/App/Models/mdlToken.swift b/Sources/App/Models/mdlToken.swift index f9c59c1..9921d7a 100644 --- a/Sources/App/Models/mdlToken.swift +++ b/Sources/App/Models/mdlToken.swift @@ -5,4 +5,43 @@ // Created by Michiel Carman on 4/16/24. // -import Foundation +import Fluent +import Vapor + +final class mdlToken: Model, Content{ + static let schema = "user_token" + + @ID(custom: "id", generatedBy: .database) var id: Int? + @Field(key: "value") var value: String + @Parent(key: "associateId") var user: mdlAssociate + + init(){} + + init(id: Int? = nil, value: String, userid: mdlAssociate.IDValue){ + self.id = id + self.value = value + self.$user.id = userid + } +} +extension mdlToken{ + struct Create: AsyncMigration { + var name: String { "CreateUserToken"} + func prepare(on database: Database) async throws { + try await database.schema("user_token") + .field("id", .int, .identifier(auto: true)) + .field("value", .string) + .field("associateId", .int, .required, .references("associate", "id")) + .unique(on: "value") + .create() + } + + func revert(on database: Database) async throws { + try await database.schema("user_token").delete() + } + } +} +extension mdlToken: ModelTokenAuthenticatable{ + static let valueKey = \mdlToken.$value + static let userKey = \mdlToken.$user + var isValid: Bool{true} +} diff --git a/Sources/App/configure.swift b/Sources/App/configure.swift index 9c2ff2d..70a8c2d 100644 --- a/Sources/App/configure.swift +++ b/Sources/App/configure.swift @@ -2,19 +2,23 @@ import NIOSSL import Fluent import FluentPostgresDriver import Vapor +//import JWT extension String { var bytes: [UInt8] { .init(self.utf8) } } - +//extension JWKIdentifier{ +// static let `public` = JWKIdentifier(string: "public") +// static let `private` = JWKIdentifier(string: "private") +//} // configures your application public func configure(_ app: Application) async throws { -// let privateKey = try String(contentsOfFile: app.directory.workingDirectory + "jwtRS256.key") +// let privateKey = try String(contentsOfFile: app.directory.workingDirectory + "jwtRS256.key").replacingOccurrences(of: "\\n", with: "\n") // let privateSigner = try JWTSigner.rs256(key: .private(pem: privateKey.bytes)) // -// let publicKey = try String(contentsOfFile: app.directory.workingDirectory + "jwtRS256.key.pub") +// let publicKey = try String(contentsOfFile: app.directory.workingDirectory + "jwtRS256.key.pub").replacingOccurrences(of: "\\n", with: "\n") // let publicSigner = try JWTSigner.rs256(key: .public(pem: publicKey.bytes)) // // app.jwt.signers.use(privateSigner, kid: .private) diff --git a/Sources/App/routes.swift b/Sources/App/routes.swift index 6963994..5ae3388 100644 --- a/Sources/App/routes.swift +++ b/Sources/App/routes.swift @@ -2,6 +2,13 @@ import Foundation import Fluent import Vapor import Crypto +//import JWT + +//struct Example: JWTPayload, Content{ +// +// var test: String +// func verify(using signer: JWTSigner) throws{} +//} func routes(_ app: Application) throws { app.routes.caseInsensitive = true @@ -11,6 +18,7 @@ func routes(_ app: Application) throws { baseApp.get { req async in "Training Services Version 1.0" } + try baseApp.register(collection: cntrlAssociate()) try baseApp.register(collection: cntrlDepartment()) try baseApp.register(collection: cntrlDocument()) diff --git a/jwtRS256.key b/jwtRS256.key new file mode 100644 index 0000000..6bbf1a3 --- /dev/null +++ b/jwtRS256.key @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIJKQIBAAKCAgEAx0IhjUkXs2RSz7bhcrISg6u+So5EdyLN/3gQK8N5CFshS9n2 +Div8QYBWTsJU/vJ8qn1g8jrZ0PCpEbvfTZVHs3Lb5N4USSUHe+JpyFup1EnOzTAx +Au4OoOSnnoiBIHv0gwunFE9lH1ey/p/ro+uBz9iIBmwtn+4xGPkcZ4QPeKOUudUL +ZEQ3DK7MNF9rcCAyts/xyrQ4mHcpHM4r8SeWt4aiblWBkdQqhQdKbUQbyrfpv3AZ +GVRoUjYkIfvp5BiCkZADj/knhxxvdgKNv9D8hOhkQ69H6uFk4jT1QsRa0GuKPCWf +ZQOxdCCpk65PlrrAXryWWPMfywry/vuMCLUJJiUVjXoYIKOzz8GNf214RhmHuW1b +aNDgkLV8EQ5GfFcWd92Tb/04eH9TI3M7iSGaIShRCbRJ8szFSzGCGOh2DNGXfj1B +PixlFP1s/Vdin9UQMXMZpQnw1IgNdMksJ8hGMH2qxo6PrJvxIgjlW1F0/m9CT9mj +2GIkYA5CwPej0VidkUgqFOeY8fGXw82YP44jAHpOO3cmC2zPNxqUN09caJj/BaNQ +D9h0mxyW/t6xVuvZzN/TmUFeZ8UOgxnqCsu9QFrah4S9JN/7ZssYI+PrWaeYYdM9 ++8QJdNzG89G+Krcfez4ZXD8RmarpMpu9Ibxn4tLwQKJUg3+i15IQYem2pEUCAwEA +AQKCAgEAnCCWF5VZC8glFvpOBke/OAqe7N4yiULtNCpISXI9aK23Fi7vnEUcv9om +RkoAvU8VHaVmDAbO2udnIi0aGDlK2DG8WMUmEfYAujI6/rHMDTnT33TOQmRzc+AV +8qM0wlY9zVyZCtH3Ayr9ZoO56FPFAC3Cz3199l11fQLcHcQdN0K9ayqiZWsJ01c3 +TCYkEdQ4yIOBCnpSCiz8OsTVK30AnEbTCS7YeoJK2rdOuvTt7dZTQhw8nltD7n7b +qwEvT/lgfVvhXrM0JKrELyxrEwxWgHFy9pHmAQWgvZoPPsSJ/9rzkpNMJKNTLRHW +rY+cbxdfWhvclk/R7o9ivSYuoDfxtN5hDkYeQ1yFMxNgYqwyjayRmwii4XILf4dd +GJtqxf7sThR57XzKs85dixUPL154a5xWQFapeXuksItw2NGp5yAQotAYjdI4Sn1q +BXn4edZf92PAKBrkboNbJma3NCOQ9ZF2pil/wvi8gjAq5osKhptsXKY2v6/XwJ2+ +duivPI9HuVheNABNKDaaktEXUwAgDl9a9xdM/CqZJjWUxZ2Ro/sepvFT1XiSIX17 +PGYTfNPSqzgbStUiUuAiimyICZXiVTEaqGtv5penG659nx9/Th1E4xnm//XbwrSm +dW8yZFEaXvMtY1lCuuMHTeRkfsvMl+oesVX4+1eAmsLmT2VVPRUCggEBAO2Bgx3P +YIUbxcGqVKP5xBG3TvWx413aprqd1qr5mGhT7mnXsTXaR66fE9gsKHxauVLZKDMK +d2lG8AQitsuT1MRUhtKSEgtmmbJnnKG1TCsueFYBFyrXS6Z4RZq7hVopaJ1Wq2IT +KmazUXPYcvJoaYaAVhlV6Ax3ayua3xu2xz0p9kDlZjNSClLXd36EANtfa+iGiPRJ +rJXWtmYz0JB946RkzfyUZDCw3Iv+LdOH3g2/kR26IwtgDN3hBdeQ6uXAYMv6qhmY +vKaHRzByWlGDrnfGZnjNT6gCILHBCTfHgGgeRdgfdRPDmxwDttW4ZGgYHTtvk+UQ +Ge2hmwUs67eRqNsCggEBANbGLy+eRA+8I72irVWLhSuv2pm7bZXGoLTtHUQme0hm +J4RatOOsar/BsEONQ5zjbSygfiNrrlAYTRuq+BKwq8+BQiSHgoAStqEozw8sYMkg +JSqZLjkr4jBY3k+Caax1Q56FgybA7zlWRJ3b51bxVXOdBOudXrsrSWYNiMGRZL9R +H8gKwaVUjAV+hRzw9WHcv6Z8wEjDBJU/erk8wWgQXp1gYfTmD7+EPcqjG1kQFR7G +NgVwNMv0nSAc2b78/aE9rofPoVNFYTZb9omGzkHqSlgzfg8RIThFj/rHkeKBZgcL +I5fWYpeHtBQ8ugGpIkAPNCWuJ0J5CR4MDaVqc71qYV8CggEBAMUqzTSEIfaeta8u +vhFamcCzUqN5R+10RRGLUtihWDrT4oqPexff275FZW4CSqHhlxQGHu8wcy08rKhi +QSoD0ZAjM8G/bad1R4bpqQg9v7vhWhjb0uXH0Y+jLPVlUzEkSyrC9of2rS6REr9/ +Iv3GXERCajeiL3+Z66hJa4pQVFqcaUg6qHtWUievpA2tfCChuK1ScoOKXfIOS2HR +BgXN3Jtu6+oRLpOVX6IAIv8GPftf2xK4obshw8y2r+ji735IFJOBxpB1UbdVem3n +oL8m+EGKvSrwFn69NXMbc5GQ1zKYXxcqFkGQE0ZQdwo5h3E7A5mB6V8x3LR/gnfr +cMv16h0CggEABO908iEu6zWllJwCY67bFo/p9BJsiD6SPtYcEA7aUmWWjuIOuhSi +N1naFRNP9HD9d0MQ4vt8e/ay/unE3ZkVuaT8wh4jD5JehAGOEIkyiCym7f2rJEEv +53CDW0xh5UakkSX8R42zcZvb48zD2imdjxSSol9xZ9eszh2NgPU4XXUdx9kHbict +OIsZv/qBiQgrl0XSnVlt54gX593Pi/akVfV7CsemoP4G9XseC8Pk2Foxr5pOfElY +uv4uusbRhT0Hn7NTv7gebcNbPPnOD5G0t6YnRW1dWA+xKVAguBXFJHBLFxgkF9tU +dgqXM6aRVOuEhUElQc/KN9iLD7wMgNPK/QKCAQA4ivEUfhbBiIqOfk4jwmnOzrBU +jycdEqFo5ZkM83/L16ukmrOTLnSTqdrEIsGry/D0K5/LOOBJDTScSA5LbFordcZm +Ofm5yXD1anWgOZUx+xR3ZF1UIVrtm2WC65iOVDrcYZsDGRFi6jtBFZuGV7+LtRhC +rF0jquh55KzaoG3c0LfECi/3FReZmT3SkIHJxyqxWYRlasJVkcKERZ10y+3KeGkv +ro5Rd+nRVCt9/4qi1bqsRd5YCAdbNSTkFEblbqcWVlcX577qUU0xOCm4fJuONVfd +vA6QjcLflZDYdO7omPxStq/rbEUBOBi8XXx/FajkC1nL9kq0ZSc9ODvluROD +-----END RSA PRIVATE KEY----- diff --git a/jwtRS256.key.pub b/jwtRS256.key.pub new file mode 100644 index 0000000..ae15faa --- /dev/null +++ b/jwtRS256.key.pub @@ -0,0 +1,14 @@ +-----BEGIN PUBLIC KEY----- +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAx0IhjUkXs2RSz7bhcrIS +g6u+So5EdyLN/3gQK8N5CFshS9n2Div8QYBWTsJU/vJ8qn1g8jrZ0PCpEbvfTZVH +s3Lb5N4USSUHe+JpyFup1EnOzTAxAu4OoOSnnoiBIHv0gwunFE9lH1ey/p/ro+uB +z9iIBmwtn+4xGPkcZ4QPeKOUudULZEQ3DK7MNF9rcCAyts/xyrQ4mHcpHM4r8SeW +t4aiblWBkdQqhQdKbUQbyrfpv3AZGVRoUjYkIfvp5BiCkZADj/knhxxvdgKNv9D8 +hOhkQ69H6uFk4jT1QsRa0GuKPCWfZQOxdCCpk65PlrrAXryWWPMfywry/vuMCLUJ +JiUVjXoYIKOzz8GNf214RhmHuW1baNDgkLV8EQ5GfFcWd92Tb/04eH9TI3M7iSGa +IShRCbRJ8szFSzGCGOh2DNGXfj1BPixlFP1s/Vdin9UQMXMZpQnw1IgNdMksJ8hG +MH2qxo6PrJvxIgjlW1F0/m9CT9mj2GIkYA5CwPej0VidkUgqFOeY8fGXw82YP44j +AHpOO3cmC2zPNxqUN09caJj/BaNQD9h0mxyW/t6xVuvZzN/TmUFeZ8UOgxnqCsu9 +QFrah4S9JN/7ZssYI+PrWaeYYdM9+8QJdNzG89G+Krcfez4ZXD8RmarpMpu9Ibxn +4tLwQKJUg3+i15IQYem2pEUCAwEAAQ== +-----END PUBLIC KEY-----