Type 'User' does not conform to protocol 'ModelAuthenticatable'

Trying to apply Vapor Doc Security -> Authentication when adding the snippet:

extension User: ModelAuthenticatable {
    static let usernameKey = \User.$email
    static let passwordHashKey = \User.$passwordHash

    func verify(password: String) throws -> Bool {
        try Bcrypt.verify(password, created: self.passwordHash)
    }
}

I get the message from Xcode:

Is it safe to Add stubs for conformance ?

If so, what exactly should be the code to be added in the closures ?

    static var usernameKey: KeyPath<User, FluentKit.FieldProperty<User, String>> {
        <#code#>
    }
    
    static var passwordHashKey: KeyPath<User, FluentKit.FieldProperty<User, String>> {
        <#code#>
    }

Sorry I am too new in Vapor to find out by myself ! Thanks for helping.

What does your user model look like?

Quite the same as in the Docs I believe :

import Fluent
import Vapor

final class User: Model, @unchecked Sendable, Content {
    static let schema = "users"
    
    @ID(key: .id)
    var id: UUID?
    
    @Field(key:"name")
    var name: String
    
    @Field(key:"password_hash")
    var passwordHash: String
    
    @Field(key:"email")
    var email: String
    
    @Children(for: \.$user)
    var todos: [Todo]
    
    init(){}
    
    init(id: UUID? = nil, name: String, email: String, passwordHash: String) {
        self.id = id
        self.name = name
        self.email = email
        self.passwordHash = passwordHash
    }
    func toDTO() -> UserDTO {
        .init(
            id: self.id,
            name: self.$name.value,
            email: self.$email.value,
            passwordHash: self.$passwordHash.value
        )
    }
}

only a children field more.

I am using Xcode 26.2. Is this not linked to the new thread safe policy ?

Ah yeah it’s the Sendable issues with key-paths. Try something like VaporAuthExample/Sources/App/Models/User.swift at main · 0xTim/VaporAuthExample · GitHub

yes @0xTim that's what I did, following assistant's tip:

extension User: ModelAuthenticatable {
    static var usernameKey: KeyPath<User, FluentKit.FieldProperty<User, String>> { \User.$email }
    
    static var passwordHashKey: KeyPath<User, FluentKit.FieldProperty<User, String>> { \User.$password }

    func verify(password: String) throws -> Bool {
        try Bcrypt.verify(password, created: self.password)
    }
}

(Note that in the meantime, for convenience, I changed passwordHash to password)

Now my problem is I do not get the login route as per Vapor Doc to run:

Here is the controller :

    func boot(routes: any RoutesBuilder) throws {
        let users = routes.grouped("users")
        users.post(use: self.create)
        let passwordProtected = users.grouped(User.authenticator(), User.guardMiddleware())
        passwordProtected.post("login", use: self.login)
    }
    
    @Sendable
    func login(req: Request) async throws -> ClientTokenResponse {
        let user = try req.auth.require(User.self)
        print(user)
        let payload = try SessionToken(with: user)
        return ClientTokenResponse(token: try await req.jwt.sign(payload))
    }

In my database I have at least one user:

Let's consider Emile. The command :

% echo -n "emile@example.com:secret" | base64

produces:
ZW1pbGVAZXhhbXBsZS5jb206c2VjcmV0

But when I send the request:
% curl -H "Authorization: Basic ZW1pbGVAZXhhbXBsZS5jb206c2VjcmV0" http://127.0.0.1:8080/users/login

I receive an error:
{"reason":"Not Found","error":true}**%**

I can't figure out what I am doing wrong. Can someone help ?

For one adding User.guardMiddleware() on the login route doesn't really make sense as that throws a 401 if the user is not logged in. You're also trying to verify a plaintext password using bcrypt, or at least that's what you showed in your DB screenshot/token creation command. Finally, regarding your 404, did you register your controller using app.register(collection: YourControllerHere()?

@ptoffy doesn't make sense ? Yet this is the snippet from Vapor Doc -> Authentication -> JWT :

let passwordProtected = app.grouped(User.authenticator(), User.guardMiddleware())
passwordProtected.post("login") { req async throws -> ClientTokenResponse in
    let user = try req.auth.require(User.self)
    let payload = try SessionToken(with: user)
    return ClientTokenResponse(token: try await req.jwt.sign(payload))
}

Regarding the paswword for Basic Authorization, I followed this section of the doc:

Should I not ?

Finally, at least I can confirm the controller is registred in the routes:

func routes(_ app: Application) throws {
    app.get { req async in
        "It works!"
    }
    app.get("hello") { req async -> String in
        "Hello, world!"
    }
    try app.register(collection: UserController())
}

So what can I do next ?

Looking at your database - is your password stored in plaintext?

Obviously, yes.
OK @0xTim I got it. Password hashigh is in the next lab !

:+1:

Just to be clear, the issue you’re hitting is that your verify function is trying to check the hash of your password provided with the password stored and they obviously don’t match because they’re stored in plaintext (and please, please, please never do this)

So I suppose a good practice would be to have a passwordHash property in User and a password one in UserDTO. And make the conversion in the toModel() function at the call of UserController's create() function.

Right?

Have a password in the CreateUserDTO but not in the UserDTO - you don’t want to be returning password hashes in your responses

Ok @0xTim that's how I did it:

I replaced the password column in Users by password_hash, and updated the migration CreateUser accordingly.

In the UserDTO however I kept the password property and modified the toModel() function as follows:

        if let password = self.password {
            model.passwordHash = try Bcrypt.hash(password)
        }

I update the user authentication verify func:

extension User: ModelAuthenticatable {
    static var usernameKey: KeyPath<User, FluentKit.FieldProperty<User, String>> { \User.$email }
    
    static var passwordHashKey: KeyPath<User, FluentKit.FieldProperty<User, String>> { \User.$passwordHash }

    func verify(password: String) throws -> Bool {
        try Bcrypt.verify(password, created: self.passwordHash)
    }
}

Which now operates on passwordHash indeed.

By the way, in the reverse function toDTO() in User model, I just set password to nil:

    func toDTO() -> UserDTO {
        .init(
            id: self.id,
            name: self.$name.value,
            email: self.$email.value,
            password: nil
        )
    }

I also updated the create() function of the UserController to decode on userDTO:

    @Sendable
    func create(req: Request) async throws -> UserDTO {
        let user = try req.content.decode(UserDTO.self).toModel()
        try await user.save(on: req.db)
        return user.toDTO()
    }

At least I managed to have the passwordHashes stored in the database, instead of the passwords themselves :relieved_face:

The bad news is I still have the same error when trying to login as previously :slightly_frowning_face:

@simon68 it looks like you're making a GET request to a POST endpoint, that's why you get a 404. You need to add -X POST to your curl request

curl -X POST \
  -H "Authorization: Basic ZW1pbGVAZXhhbXBsZS5jb206c2VjcmV0" \
  http://127.0.0.1:8080/users/login

ok @ptoffy I thought it had to be something stupid, and it was ! Thanks.