I am following the book 'Server Side Swift with Vapor' and I am at chapter 18. I followed the instructions on how to generate a token when the user logs in. I created a Token class:
import Vapor
import Fluent
final class Token: Model, Content {
static let schema = "tokens"
@ID
var id: UUID?
@Field(key: "value")
var value: String
@Parent(key: "userID")
var user: User
init() {}
init(id: UUID? = nil, value: String, userID: User.IDValue) {
self.id = id
self.value = value
self.$user.id = userID
}
}
extension Token {
static func generate(for user: User) throws -> Token {
let random = [UInt8].random(count: 16).base64
return try Token(value: random, userID: user.requireID())
}
}
extension Token: ModelTokenAuthenticatable {
typealias User = App.User
static let valueKey = \Token.$value
static let userKey = \Token.$user
var isValid: Bool {
true
}
}
And this is the Token migration:
import Fluent
struct CreateToken: Migration {
func prepare(on database: Database) -> EventLoopFuture<Void> {
database.schema("tokens")
.id()
.field("value", .string, .required)
.field("userID",
.uuid,
.required,
.references("users", "id", onDelete: .cascade))
.create()
}
func revert(on database: Database) -> EventLoopFuture<Void> {
database.schema("tokens").delete()
}
}
And this is my login route:
func loginHandler(_ req: Request) throws -> EventLoopFuture<Token> {
let user = try req.auth.require(User.self)
let token = try Token.generate(for: user)
return token.save(on: req.db).map {
token
}
}
And this is how I configure the login route:
let basicAuthGroup = usersRoutes.grouped(basicAuthMiddleware)
basicAuthGroup.post("login", use: loginHandler)
I also have sign up route that is not protected and can be used without being authenticated. What I miss is the step before logging in. Indeed Vapor throws an exception if the user is not authenticated. In alamofire, this is the login method:
func login(user: User) async throws -> AuthToken {
return try await withCheckedThrowingContinuation { continuation in
Task {
let headers = HTTPHeaders([
HTTPHeader(name: "Authorization", value: "Basic \(user.token!)")
])
AF.request(baseUrl + "login", method: .post, parameters: user, headers: headers)
.responseData { response in
switch response.result {
case .success(let data):
do {
let token = try JSONDecoder().decode(AuthToken.self, from: data)
continuation.resume(returning: token)
} catch {
continuation.resume(throwing: error)
}
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}
}
The method fails I can see from the server that this is the failing line of code:
let user = try req.auth.require(User.self)
I can see from the book that he does this in two steps:
The steps are shown below:
How to do this in Alamofire? I tried using the authenticate() method like this:
func authenticate(user: User) async throws {
return try await withCheckedThrowingContinuation { continuation in
Task {
AF.request(baseUrl, method: .get).authenticate(username: user.username, password: user.password!)
.responseString { response in
switch response.result {
case .success(let json):
print(json)
continuation.resume()
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}
}
But if I print the json data, it doesn't return any authentication token. It just returns a list of users and I think that the default users GET route gets called instead. I am probably missing something or one step is missing in the book. How to obtain the basic auth token?
You've made your login route expect a token (with your login route function), but when you send the basic authentication (username/password) it doesn't know how to use that information to look up a valid user (since it's expecting a token).
There are a few steps here that are necessary. I think you've done most of em but here's a full guide from the Vapor Docs:
First you need a User model in the database:
import Fluent
import Vapor
final class User: Model, Content {
static let schema = "users"
@ID(key: .id)
var id: UUID?
@Field(key: "name")
var name: String
@Field(key: "email")
var email: String
@Field(key: "password_hash")
var passwordHash: String
init() { }
init(id: UUID? = nil, name: String, email: String, passwordHash: String) {
self.id = id
self.name = name
self.email = email
self.passwordHash = passwordHash
}
}
And a migration:
import Fluent
import Vapor
extension User {
struct Migration: AsyncMigration {
var name: String { "CreateUser" }
func prepare(on database: Database) async throws {
try await database.schema("users")
.id()
.field("name", .string, .required)
.field("email", .string, .required)
.field("password_hash", .string, .required)
.unique(on: "email")
.create()
}
func revert(on database: Database) async throws {
try await database.schema("users").delete()
}
}
}
In your main file:
app.migrations.add(User.Migration())
Then what you want is a /signup
route (that is open), and takes a username and password (and anything else you need to create a user in the database):
import Vapor
extension User {
struct Create: Content {
var name: String
var email: String
var password: String
var confirmPassword: String
}
}
extension User.Create: Validatable {
static func validations(_ validations: inout Validations) {
validations.add("name", as: String.self, is: !.empty)
validations.add("email", as: String.self, is: .email)
validations.add("password", as: String.self, is: .count(8...))
}
}
Sign up, in your routes (POST /users/
):
app.post("users") { req async throws -> User in
try User.Create.validate(content: req)
let create = try req.content.decode(User.Create.self)
guard create.password == create.confirmPassword else {
throw Abort(.badRequest, reason: "Passwords did not match")
}
let user = try User(
name: create.name,
email: create.email,
passwordHash: Bcrypt.hash(create.password)
)
try await user.save(on: req.db)
return user
}
Then you need to create a /login
route that takes a valid username and password (via a ModelAuthenticatable
extension on your User
model).
import Fluent
import Vapor
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)
}
}
This is where the magic happens, and where your code differs:
let passwordProtected = app.grouped(User.authenticator())
passwordProtected.post("login") { req -> User in
try req.auth.require(User.self)
}
THEN you can create a UserToken model and return a token string on login:
import Fluent
import Vapor
final class UserToken: Model, Content {
static let schema = "user_tokens"
@ID(key: .id)
var id: UUID?
@Field(key: "value")
var value: String
@Parent(key: "user_id")
var user: User
init() { }
init(id: UUID? = nil, value: String, userID: User.IDValue) {
self.id = id
self.value = value
self.$user.id = userID
}
}
extension UserToken {
struct Migration: AsyncMigration {
var name: String { "CreateUserToken" }
func prepare(on database: Database) async throws {
try await database.schema("user_tokens")
.id()
.field("value", .string, .required)
.field("user_id", .uuid, .required, .references("users", "id"))
.unique(on: "value")
.create()
}
func revert(on database: Database) async throws {
try await database.schema("user_tokens").delete()
}
}
}
extension User {
func generateToken() throws -> UserToken {
try .init(
value: [UInt8].random(count: 16).base64,
userID: self.requireID()
)
}
}
Now modify the login route to return the token:
let passwordProtected = app.grouped(User.authenticator())
passwordProtected.post("login") { req async throws -> UserToken in
let user = try req.auth.require(User.self)
let token = try user.generateToken()
try await token.save(on: req.db)
return token
}
This is where you would send the Alamofire .authenticate
request with the username and password. Expect a token returned.
AND THEN you can FINALLY authenticate your OTHER routes using the token with a ModelTokenAuthenticatable
conforming object.
import Vapor
import Fluent
extension UserToken: ModelTokenAuthenticatable {
static let valueKey = \UserToken.$value
static let userKey = \UserToken.$user
var isValid: Bool {
true
}
}
And for the FINALE: Make a new route that authenticates using your token you got from the /login
route.
let tokenProtected = app.grouped(UserToken.authenticator())
tokenProtected.get("me") { req -> User in
try req.auth.require(User.self)
}
Use this route by adding a Bearer token header with the token you got from the login route. I like to store the token in the device's keychain for future reference (until the token expires). You can even sync it with iCloud and everything should magically stay logged in across devices!
Check out an encrypted chat app I made with this pattern using Vapor/SwiftUI/Alamofire: Affirmate