Skip to content

Instantly share code, notes, and snippets.

@tanner0101
Last active May 2, 2024 14:24
Show Gist options
  • Save tanner0101/a9aee81a7b9feb879b355101ea7a0661 to your computer and use it in GitHub Desktop.
Save tanner0101/a9aee81a7b9feb879b355101ea7a0661 to your computer and use it in GitHub Desktop.
Example of authentication with Fluent in Vapor 4
import Fluent
import Vapor
func routes(_ app: Application) throws {
app.post("users") { req -> EventLoopFuture<User> in
try User.Create.validate(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)
)
return user.save(on: req.db)
.map { user }
}
let passwordProtected = app.grouped(User.authenticator().middleware())
passwordProtected.post("login") { req -> EventLoopFuture<Token> in
let user = try req.auth.require(User.self)
let token = try user.generateToken()
return token.save(on: req.db)
.map { token }
}
let tokenProtected = app.grouped(Token.authenticator().middleware())
tokenProtected.get("me") { req -> User in
try req.auth.require(User.self)
}
}
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: .count(3...) && .alphanumeric)
validations.add("email", as: String.self, is: .email)
validations.add("password", as: String.self, is: .count(8...))
}
}
final class User: Model, Content, Authenticatable {
static let schema = "users"
@ID(key: "id")
var id: Int?
@Field(key: "name")
var name: String
@Field(key: "email")
var email: String
@Field(key: "password_hash")
var passwordHash: String
func generateToken() throws -> Token {
try .init(value: [UInt8].random(count: 16).base64, userID: self.requireID())
}
init() { }
init(id: Int? = nil, name: String, email: String, passwordHash: String) {
self.id = id
self.name = name
self.email = email
self.passwordHash = passwordHash
}
}
extension User: ModelUser {
static let usernameKey = \User.$email
static let passwordHashKey = \User.$passwordHash
func verify(password: String) throws -> Bool {
try Bcrypt.verify(password, created: self.passwordHash)
}
}
final class Token: Model, Content {
static let schema = "tokens"
@ID(key: "id")
var id: Int?
@Field(key: "value")
var value: String
@Parent(key: "user_id")
var user: User
init() { }
init(id: Int? = nil, value: String, userID: User.IDValue) {
self.id = id
self.value = value
self.$user.id = userID
}
}
extension Token: ModelUserToken {
static let valueKey = \Token.$value
static let userKey = \Token.$user
var isValid: Bool { true }
}
extension User {
struct Migration: Fluent.Migration {
func prepare(on database: Database) -> EventLoopFuture<Void> {
database.schema("users")
.field("id", .int, .identifier(auto: true))
.field("name", .string, .required)
.field("email", .string, .required)
.field("password_hash", .string, .required)
.create()
}
func revert(on database: Database) -> EventLoopFuture<Void> {
database.schema("users").delete()
}
}
}
extension Token {
struct Migration: Fluent.Migration {
func prepare(on database: Database) -> EventLoopFuture<Void> {
database.schema("tokens")
.field("id", .int, .identifier(auto: true))
.field("value", .string, .required)
.field("user_id", .int, .required)
.unique(on: "value")
.create()
}
func revert(on database: Database) -> EventLoopFuture<Void> {
database.schema("tokens").delete()
}
}
}
@przemyslaw-szurmak
Copy link

przemyslaw-szurmak commented Jan 23, 2020

hey @tanner0101 aren't you missing isValid property in Token extension where you declare conformance with ModelUserToken ?

besides, very cool example ! thanks for sharing !

@tanner0101
Copy link
Author

@przemyslaw-szurmak good catch, added. Thanks :)

@ozzyq7
Copy link

ozzyq7 commented Jan 23, 2020

Hi @tanner0101 thank you for sharing. Will there also be documentation on sessions?

@tanner0101
Copy link
Author

@ozzyq7 yup! Session docs are on the todo still.

@ChrisNg02655332
Copy link

Thank for sharing. I tried to migrate manually the user with bcrypt hash and it’s not working. Could you please add an example for migration?

@kevintafo
Copy link

hi @tanner0101. What's ModelUserToken and ModelUser ?

@kevintafo
Copy link

Ok it's now ModelAuthentificable & ModelTokenAuthentificable

@jacrack
Copy link

jacrack commented May 22, 2021

some example of WebCredentialsAuth and Sessions please, thanks so much :)

@zjonejj
Copy link

zjonejj commented Aug 15, 2021

Ok it's now ModelAuthentificable & ModelTokenAuthentificable

It's now: ModelTokenAuthenticatable
not : ModelTokenAuthentificable

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment