Skip to content

Commit

Permalink
Add asyncCredentialsAuthenticator to `ModelCredentialsAuthenticatab…
Browse files Browse the repository at this point in the history
…le` (#744)

* Extend ModelCredentialsAuthenticatable with asyncCredentialsAuthenticator #743

* compiler guards for test
  • Loading branch information
fatto authored Aug 16, 2022
1 parent 469310c commit 26c4460
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -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<Self>(database: database)
}
}

@available(macOS 12, iOS 15, watchOS 8, tvOS 15, *)
private struct AsyncModelCredentialsAuthenticator<User>: 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

60 changes: 60 additions & 0 deletions Tests/FluentTests/CredentialTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit 26c4460

Please sign in to comment.