From 26c446002f03c5ab34b20d86873014ef3d92d0da Mon Sep 17 00:00:00 2001 From: Marco Fattorel Date: Tue, 16 Aug 2022 18:22:15 +0200 Subject: [PATCH] Add `asyncCredentialsAuthenticator` to `ModelCredentialsAuthenticatable` (#744) * Extend ModelCredentialsAuthenticatable with asyncCredentialsAuthenticator #743 * compiler guards for test --- ...edentialsAuthenticatable+Concurrency.swift | 33 ++++++++++ Tests/FluentTests/CredentialTests.swift | 60 +++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 Sources/Fluent/Concurrency/ModelCredentialsAuthenticatable+Concurrency.swift diff --git a/Sources/Fluent/Concurrency/ModelCredentialsAuthenticatable+Concurrency.swift b/Sources/Fluent/Concurrency/ModelCredentialsAuthenticatable+Concurrency.swift new file mode 100644 index 00000000..7381bbfa --- /dev/null +++ b/Sources/Fluent/Concurrency/ModelCredentialsAuthenticatable+Concurrency.swift @@ -0,0 +1,33 @@ +#if compiler(>=5.5) && canImport(_Concurrency) +import NIOCore +import Vapor + +@available(macOS 12, iOS 15, watchOS 8, tvOS 15, *) +extension ModelCredentialsAuthenticatable { + public static func asyncCredentialsAuthenticator( + _ database: DatabaseID? = nil + ) -> AsyncAuthenticator { + AsyncModelCredentialsAuthenticator(database: database) + } +} + +@available(macOS 12, iOS 15, watchOS 8, tvOS 15, *) +private struct AsyncModelCredentialsAuthenticator: AsyncCredentialsAuthenticator + where User: ModelCredentialsAuthenticatable +{ + typealias Credentials = ModelCredentials + + public let database: DatabaseID? + + func authenticate(credentials: ModelCredentials, for request: Request) async throws { + if let user = try await User.query(on: request.db(self.database)).filter(\._$username == credentials.username).first() { + guard try user.verify(password: credentials.password) else { + return + } + request.auth.login(user) + } + } +} + +#endif + diff --git a/Tests/FluentTests/CredentialTests.swift b/Tests/FluentTests/CredentialTests.swift index 82257691..fcf23754 100644 --- a/Tests/FluentTests/CredentialTests.swift +++ b/Tests/FluentTests/CredentialTests.swift @@ -60,9 +60,69 @@ final class CredentialTests: XCTestCase { XCTAssertEqual(res.status, .ok) } } + } + +#if compiler(>=5.5) && canImport(_Concurrency) + @available(macOS 12, iOS 15, watchOS 8, tvOS 15, *) + func testAsyncCredentialsAuthentication() async throws { + let app = Application(.testing) + defer { app.shutdown() } + + + // Setup test db. + let testDB = ArrayTestDatabase() + + app.databases.use(testDB.configuration, as: .test) + + // Configure sessions. + app.middleware.use(app.sessions.middleware) + + // Setup routes. + let sessionRoutes = app.grouped(CredentialsUser.sessionAuthenticator()) + + let credentialRoutes = sessionRoutes.grouped(CredentialsUser.asyncCredentialsAuthenticator()) + credentialRoutes.post("login") { req -> Response in + guard req.auth.has(CredentialsUser.self) else { + throw Abort(.unauthorized) + } + return req.redirect(to: "/protected") + } + + let protectedRoutes = sessionRoutes.grouped(CredentialsUser.redirectMiddleware(path: "/login")) + protectedRoutes.get("protected") { req -> HTTPStatus in + _ = try req.auth.require(CredentialsUser.self) + return .ok + } + // Create user + let password = "password-\(Int.random())" + let passwordHash = try Bcrypt.hash(password) + let testUser = CredentialsUser(id: UUID(), username: "user-\(Int.random())", password: passwordHash) + testDB.append([TestOutput(testUser)]) + testDB.append([TestOutput(testUser)]) + testDB.append([TestOutput(testUser)]) + testDB.append([TestOutput(testUser)]) + + // Test login + let loginData = ModelCredentials(username: testUser.username, password: password) + try app.test(.POST, "/login", beforeRequest: { req in + try req.content.encode(loginData, as: .urlEncodedForm) + }) { res in + XCTAssertEqual(res.status, .seeOther) + XCTAssertEqual(res.headers[.location].first, "/protected") + let sessionID = try XCTUnwrap(res.headers.setCookie?["vapor-session"]?.string) + // Test accessing protected route + try app.test(.GET, "/protected", beforeRequest: { req in + var cookies = HTTPCookies() + cookies["vapor-session"] = .init(string: sessionID) + req.headers.cookie = cookies + }) { res in + XCTAssertEqual(res.status, .ok) + } + } } +#endif } final class CredentialsUser: Model {