Last active
May 2, 2024 14:24
-
-
Save tanner0101/a9aee81a7b9feb879b355101ea7a0661 to your computer and use it in GitHub Desktop.
Example of authentication with Fluent in Vapor 4
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 good catch, added. Thanks :)
Hi @tanner0101 thank you for sharing. Will there also be documentation on sessions?
@ozzyq7 yup! Session docs are on the todo still.
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?
hi @tanner0101. What's ModelUserToken and ModelUser ?
Ok it's now ModelAuthentificable & ModelTokenAuthentificable
some example of WebCredentialsAuth and Sessions please, thanks so much :)
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
hey @tanner0101 aren't you missing
isValid
property inToken
extension where you declare conformance withModelUserToken
?besides, very cool example ! thanks for sharing !