This tutorial will cover how to authenticate users with an email/username and password using HTTP Basic Authentication in Vapor 3.
BasicAuthenticatable
ProtocolSuppose you have a User.swift
, such as:
final class User: PostgreSQLModel {
var id: Int?
var email: String
var password: String
init(email: String, password: String) {
self.email = email
self.password = password
}
}
The first thing we need to to, is to conform to the BasicAuthenticatable protocol, like so:
extension User: BasicAuthenticatable {
static var usernameKey: UsernameKey { return \User.email }
static var passwordKey: PasswordKey { return \User.password }
}
We have to conform to this protocol, because the Auth package needs to know which properties on User
it should use to authenticate against. For example, some people may want to authenticate using user.username
instead of user.email
, as I am doing here.
Create the following structs that will represent the data we wish to receive from the login request, as well as the data we want to pass back to the authenticated client:
extension User {
struct AuthenticatedUser: Content {
var email: String
var id: Int
}
struct LoginRequest: Content {
var email: String
var password: String
}
}
AuthenticatedUser
is the struct which will contain the data we want to send back to the user once they are authenticated. We cannot simply use an instance of our User
class, because then it will also pass back the hashed password along with it, and that is insecure. We create the LoginRequest
struct to decode the JSON data our application will accept when the user logs in with their credentials.
Now, in our controller, we can add the following methods to create and log in a user:
final class UserController {
func createUser(_ request: Request) throws -> Future { // 1
let futureUser = try request.content.decode(User.self) // 2
// 3
return futureUser.flatMap(to: User.AuthenticatedUser.self) { (user) -> Future in
let hasher = try request.make(BCryptDigest.self)
let passwordHashed = try hasher.hash(user.password)
let newUser = User(email: user.email, password: passwordHashed) // 4
return newUser.save(on: request).map(to: User.AuthenticatedUser.self) { authedUser in // 5
return try User.AuthenticatedUser(email: authedUser.email, id: authedUser.requireID()) // 6
}
}
}
func loginUser(_ request: Request) throws -> User.AuthenticatedUser {
let user = try request.requireAuthenticated(User.self) // 7
return try User.AuthenticatedUser(email: user.email, id: user.requireID()) // 8
}
}
That's quite a lot of code, let's explain it some:
user.email
, user.password
, etc..flatMap
on our futureUser object, and this gives us access to an actual User object inside the closure. Note though, that with flatMap
, the closure must still return a future--not an instance of Usermap
on the result of the save
call. We call .map
here, because we want the closure to return an instance of User.AuthenticatedUser
, not a future. Remember the rule: return a future: use flatMap. return a non-future: use map.User
In configure.swift
, add import Authentication
and add the following line where you register your services:
try services.register(AuthenticationProvider())
You will need to add import Authentication
to your User.swift and UserController.swift as well.
The last step, is we must add the middleware to our routes. Add the following to your routes:
let userController = UserController()
router.post("createUser", use: userController.createUser) // 1
let middleWare = User.basicAuthMiddleware(using: BCryptDigest()) // 2
let authedGroup = router.grouped(middleWare) // 3
authedGroup.post("login", use: userController.loginUser) // 4
authedGroup
will now require a username/email and base64 encoded password be sent in the Authorization Header. (in the format: Authorization Basic YW5hcGFpfdeDfgfM6czc3vcmQ=
)That's all for this tutorial. Plenty more to come :) Happy coding!