2024-12-02

Vapor Swift - Bearer Authentication

How to setup a bearer authent with Vapor Swift


Setting Up ModelsLink icon

To get started, we will define two models: User as a Fluent Model and Token, which represents a bearer token. The User model will handle user credentials, while the Token model will manage authentication tokens.

Defining the User ModelLink icon

The User model represents a user in our application. It conforms to Model for database interactions and Content for Codable support.

import Vapor
import Fluent

final class User: Model, Content, @unchecked Sendable {
    static let schema = "users"
    
    @ID(key: .id)
    var id: UUID?
    
    @Field(key: "username")
    var username: String
    
    @Field(key: "passwordHash")
    var passwordHash: String
    
    init() {}
    
    init(id: UUID? = nil, username: String, passwordHash: String) {
        self.id = id
        self.username = username
        self.passwordHash = passwordHash
    }
}

[!NOTE] The @unchecked Sendable attribute is necessary for compatibility with Swift 6. For more details, see the Vapor blog post on Sendable.

Making User Conform to ModelAuthenticatableLink icon

To enable user authentication, we need to implement ModelAuthenticatable.

extension User: ModelAuthenticatable {
    static let usernameKey = \User.$username
    static let passwordHashKey = \User.$passwordHash
    
    func verify(password: String) throws -> Bool {
        try Bcrypt.verify(password, created: self.passwordHash)
    }
}

Defining the Token ModelLink icon

The Token model will manage authentication tokens for users, allowing them to authenticate using bearer tokens.

import Fluent
import Vapor

final class Token: Model, Content, @unchecked Sendable {
    static let schema = "tokens"
    
    @ID(key: .id)
    var id: UUID?
    
    @Field(key: "value")
    var value: String
    
    @Parent(key: "userID")
    var user: User
    
    @Field(key: "expiresAt")
    var expiresAt: Date?
    
    init() {}
    
    init(id: UUID? = nil, value: String, userID: UUID, expiresAt: Date? = nil) {
        self.id = id
        self.value = value
        self.$user.id = userID
        self.expiresAt = expiresAt
    }
}

Adding MigrationsLink icon

To persist these models in the database, create migrations for both User and Token.

User Migration

import Fluent

extension User {
    struct Migration: AsyncMigration {
        func prepare(on database: Database) async throws {
            try await database.schema(User.schema)
                .id()
                .field("username", .string, .required)
                .field("passwordHash", .string, .required)
                .unique(on: "username")
                .create()
        }
        
        func revert(on database: Database) async throws {
            try await database.schema(User.schema).delete()
        }
    }
}

Token Migration

extension Token {
    struct Migration: AsyncMigration {
        func prepare(on database: Database) async throws {
            try await database.schema(Token.schema)
                .id()
                .field("value", .string, .required)
                .field("userID", .uuid, .required, .references(User.schema, .id))
                .field("expiresAt", .datetime)
                .unique(on: "value")
                .create()
        }
        
        func revert(on database: Database) async throws {
            try await database.schema(Token.schema).delete()
        }
    }
}

Registering MigrationsLink icon

Add the migrations to your application configuration:

public func configure(_ app: Application) throws {
    // Register migrations
    app.migrations.add(User.Migration())
    app.migrations.add(Token.Migration())
    
    // Additional setup...
}

Authentication ControllerLink icon

The AuthController handles user authentication and token generation.

import Fluent
import Vapor

struct AuthController: RouteCollection {
    func boot(routes: RoutesBuilder) throws {
        let auth = routes.grouped("auth")
        auth.post("login", use: login)
    }
    
    @Sendable
    func login(req: Request) async throws -> TokenResponse {
        let loginData = try req.content.decode(UserDataRequest.self)
        
        // Find user by username
        guard let user = try await User.query(on: req.db)
            .filter(\.$username == loginData.username)
            .first()
        else {
            throw Abort(.unauthorized, reason: "Invalid username or password.")
        }
        
        // Verify password
        guard try user.verify(password: loginData.password) else {
            throw Abort(.unauthorized, reason: "Invalid username or password.")
        }
        
        // Generate and store token
        let tokenString = UUID().uuidString
        let token = Token(value: tokenString, userID: try user.requireID())
        try await token.save(on: req.db)
        
        return TokenResponse(token: tokenString)
    }
}

struct TokenResponse: Content {
    let token: String
}

struct UserDataRequest: Content {
    let username: String
    let password: String
}

Signup route to register usersLink icon

The signup route allows new users to register by providing a username and password. Here’s how the implementation works:

struct AuthController: RouteCollection {
  // ...
  
  @Sendable
  func signup(req: Request) async throws -> HTTPStatus {
    // Decode signup request
    let signupData = try req.content.decode(UserDataRequest.self)
    
    // Check if username already exists
    let existingUser = try await User.query(on: req.db)
      .filter(\.$username == signupData.username)
      .first()
    
    guard existingUser == nil else {
      throw Abort(.conflict, reason: "Username already exists")
    }
    
    // Create new user with hashed password
    let passwordHash = try Bcrypt.hash(signupData.password)
    let user = User(username: signupData.username, passwordHash: passwordHash)
    
    // Save user to database
    try await user.save(on: req.db)
    
    // Return successful response
    return .created
  }
}

Bearer Authentication MiddlewareLink icon

The middleware verifies the provided token in the Authorization header.

struct AuthController: RouteCollection {
    // Other methods...
    
    @Sendable
    func signup(req: Request) async throws -> HTTPStatus {
        // Decode the signup request payload
        let signupData = try req.content.decode(UserDataRequest.self)
        
        // Validate the request data (e.g., non-empty username and password)
        guard !signupData.username.isEmpty, !signupData.password.isEmpty else {
            throw Abort(.badRequest, reason: "Username and password must not be empty.")
        }
        
        // Check if the username already exists in the database
        let existingUser = try await User.query(on: req.db)
            .filter(\.$username == signupData.username)
            .first()
        
        guard existingUser == nil else {
            throw Abort(.conflict, reason: "Username already exists.")
        }
        
        // Hash the password before storing it
        let passwordHash = try Bcrypt.hash(signupData.password)
        let user = User(username: signupData.username, passwordHash: passwordHash)
        
        // Save the new user to the database
        try await user.save(on: req.db)
        
        // Respond with a 201 Created status
        return .created
    }
}

Protecting EndpointsLink icon

Use the BearerAuthenticator middleware to protect routes that require authentication.

func routes(_ app: Application) throws {
    let authController = AuthController()
    try app.register(collection: authController)
    
    let protected = app.grouped(BearerAuthenticator())
    protected.get("profile", use: userProfile)
}

func userProfile(req: Request) async throws -> User.Public {
    let user = try req.auth.require(User.self)
    return user.convertToPublic()
}

extension User {
    struct Public: Content {
        let id: UUID?
        let username: String
    }

    func convertToPublic() -> Public {
        Public(id: self.id, username: self.username)
    }
}

Testing the ApplicationLink icon

Here’s how to test the /auth/signup and /auth/login endpoints, as well as protected routes like /profile.

Create a UserLink icon

Send a POST request to /auth/signup with the following payload:

curl --location 'localhost:8080/auth/signup' \
--data '{
    "username": "admin",
    "password": "p@ssw0rd"
}'

Expected Response

Status Code: 201 Created

Obtain a TokenLink icon

Send a POST request to /auth/login with the following JSON payload:

curl --location 'localhost:8080/auth/login' \
--data '{
    "username": "admin",
    "password": "p@ssw0rd"
}'

Expected Response

  • Status Code: 200 OK
  • Body:
{
    "token": "generated_token"
}

Access a Protected RouteLink icon

Use the token from the previous step to access a protected route, such as /profile. Send a GET request with the Authorization header:

curl --location 'localhost:8080/profile' \
--header 'Authorization: Bearer •••••••••••••••••••••'

Expected Response

  • Status Code: 200 OK
  • Body:
{
    "id": "FA7816E9-F837-4C36-B2DC-1F002BDA209C",
    "username": "admin2"
}

Additional Notes for Testing:Link icon

  • Validation Errors: Test scenarios where invalid or incomplete data is submitted (e.g., missing username or password).
  • Conflict Handling: Attempt to register a user with a username that already exists to verify the 409 Conflict response.
  • Token Expiration: If token expiration is implemented, ensure expired tokens are rejected when accessing protected routes.

These steps ensure that both registration and authentication flows are working as expected.

EnhancementsLink icon

  • Password Security: Always hash passwords using a strong algorithm like Bcrypt.
  • Token Management: Use JWT for more scalable authentication systems.
  • Token Expiration: Ensure tokens have a clear expiry policy.
  • Error Handling: Expand error handling and input validation to meet your application’s needs.