Swift
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Optionals
-
Section 4: Object-Oriented and Value Types
-
Section 5: Memory Management
-
Section 6: Generics and Error Handling
-
Section 7: Concurrency
-
Section 8: Working with Collections
-
Section 9: Codable and Data Handling
-
Section 10: Protocol-Oriented Programming
-
Section 11: Testing and Tooling
-
Section 12: Practical Projects
-
Section 13: Interview Practice
-
Section 14: More Practice Exercises
-
Section 15: More Standard Library
-
Section 16: Advanced Concurrency
-
Section 17: Foundation Framework Deep Dive
-
Section 18: URLSession and Networking Deep Dive
-
Section 19: Combine Framework
-
Section 20: SwiftUI Fundamentals for Swift Developers
-
Section 21: Server-Side Swift with Vapor
-
Section 22: Swift Package Manager Deep Dive
-
Section 23: Swift Concurrency Deep Dive
-
Section 24: More Language Features
-
Section 25: Error Handling Deep Dive
-
Section 26: Testing Deep Dive
-
Section 27: Data Structures and Algorithms in Swift
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Swift Macros (Swift 5.9+)
-
Section 31: Property Wrappers Ecosystem
-
Section 32: Swift Interop Deep Dive
-
Section 33: iOS App Architecture Patterns
-
Section 34: Performance and Debugging
-
Section 35: App Distribution and CI/CD
-
Section 36: More Practical Projects
-
Section 37: SwiftData and Persistence
-
Section 38: More Design Patterns
-
Section 39: More Review and Practice
-
Section 40: More Foundation Deep Dive
-
Section 41: Advanced Collections in Swift
-
Section 42: Advanced Generics Practice
-
Section 43: UIKit for Legacy and Hybrid Apps
-
Section 44: watchOS and visionOS Development Basics
-
Section 45: More Networking Patterns
-
Section 46: More Testing Practice
-
Section 47: Accessibility in Swift Apps
-
Section 48: Localization
-
Section 49: More Practical Projects Round 2
-
Section 50: Swift Charts Framework
-
Section 51: More Interview and Algorithm Practice
-
Section 52: Final Practice and Mastery
-
Section 53: Swift Compiler and Build System
-
Section 54: More Concurrency Practice
-
Section 55: App Store Guidelines and Review
-
Section 56: More Design and Architecture
98: Authentication in Vapor Applications
I've got this "Secret Notes" API almost finished, but I've run into a glaring problem: right now, any client that knows a note's ID can read or delete it. There's no identity check. If I'm building this for a production environment, that's essentially an invitation for a data breach. We need to lock this down so only the person who created the note can touch it.
The Open Door Problem
Right now, my route looks something like this:
app.get("notes", ":noteID") { req -> EventLoopFuture<Note> in
let id = req.parameters.get("noteID", as: UUID.self)
return Note.find(id, on: req.db).unwrap(or: Abort(.notFound))
}
If I hit this endpoint with CURL, it just works. No questions asked. I want to change this so the request must prove who the user is first. In Vapor, the standard way to handle this is through the Authenticatable protocol. I'll start by updating my User model.
struct User: Model, Authenticatable {
static let schema = "users"
// ... id, username, password hash ...
}
Adding Authenticatable tells Vapor, "This model can be used to represent a logged-in user." But just adding the protocol doesn't actually stop anyone from accessing the route. I tried adding req.auth.get(User.self) inside the handler, but it just returned nil because I hadn't told Vapor how to find the user from the request.
Finding the Identity in the Header
Since this is a stateless API, I don't want to deal with cookies or sessions. I'll use JWTs (JSON Web Tokens). I've added the JWT package to my Package.swift, but now I need a payload. I realized I can't just use the User model directly as the token because the token needs to be a lightweight piece of data, not a database object.
struct UserPayload: JWTPayload {
var subject: SubjectClaim
var expiration: ExpirationClaim
func verify(using signer: JWTSigner) throws {
try expiration.verifyNotExpired()
}
}
Now, I need a way to bridge the gap between a valid token and my User model. I'll create a custom authenticator. I tried doing this manually inside the route handler at first, but that's a nightmare for maintenance. Instead, I'll use a Request extension or a middleware. Actually, Vapor has a built-in JWTAuthenticator that does the heavy lifting.
struct UserAuthenticator: JWTAuthenticator {
typealias Payload = UserPayload
func authenticate(jwt: UserPayload, for request: Request) -> EventLoopFuture<Void> {
return User.query(on: request.db)
.filter(\.$id == jwt.subject.value)
.first()
.map { user in
if let user = user {
request.auth.login(user)
}
}
}
}
I noticed a bug here during testing: if the token was valid but the user had been deleted from the database, the request still proceeded. I had to make sure request.auth.login(user) only happened if the user actually existed. If it's nil, the authentication fails, which is exactly what we want.
Locking the Door with Middleware
Now that I have an authenticator, I can't just apply it to every route—I still need a way for new users to sign up or log in without being authenticated. I'll create a grouped set of routes that are "guarded."
let protected = app.grouped(UserAuthenticator())
.grouped(User.guardMiddleware())
protected.get("notes", ":noteID") { req -> EventLoopFuture<Note> in
let user = try req.auth.require(User.self)
let id = req.parameters.get("noteID", as: UUID.self)
return Note.find(id, on: req.db)
.unwrap(or: Abort(.notFound))
.flatMapThrowing { note in
guard note.userID == user.id else {
throw Abort(.forbidden)
}
return note
}
}
I spent a few minutes wondering why my request was still hitting the handler even when I didn't provide a token. I realized I forgot the User.guardMiddleware(). The UserAuthenticator simply tries to log the user in; it doesn't actually block the request if it fails. The guardMiddleware is the bouncer that actually throws a 401 Unauthorized if the authentication didn't work.
One last detail: the logic note.userID == user.id is crucial. Authentication proves who you are, but Authorization proves what you're allowed to do. Even with a valid token, I shouldn't be able to read someone else's notes.
📋 Practical Task
Building a Restricted Admin Analytics Endpoint
You are tasked with adding an administrative feature to the Secret Notes API. You need to create a route GET /admin/stats that returns the total number of notes stored in the database across all users.
- Requirement 1: The route must be protected by the
UserAuthenticatorandUser.guardMiddleware()so that only logged-in users can access it. - Requirement 2: Implement a check to ensure that only users with an
isAdminboolean property set totruecan access this route. If a regular user attempts to access it, return a403 Forbiddenerror. - Requirement 3: The endpoint should return a simple JSON object containing the count of all notes in the
Notetable.
There are no comments for now.