In this tutorial, we'll go over web authentication using sessions. To get started, first download the starter project from GitHub here.
I typically don't like using starter projects, and prefer to show you everything from scratch, but web authentication is a bit more involved and requires a certain amount of (non auth related) setup just to demo, so for this one we have a starter project :)
Right now, the starter project has the following:
The general flow for setting up session auth in our app is the following:
The first thing we need to do, is setup our app to use sessions. To do so, go to configure.swift
, and uncomment lines 22 and 39. They read:
middlewares.use(SessionsMiddleware.self)
config.prefer(MemoryKeyedCache.self, for: KeyedCache.self)
This allows us to use sessions within our app, and enables the session middleware that we will later use to retrieve our user for a given session.
If you are using Postgres, simply create the database for this app (defined in configure.swift, line 28) by running the following in terminal:
$ createdb webauth
When a request comes into our server, we need to identify who made the request. To do so, we'll use sessions. The idea is that when a user logs in, we will store their id
in a session cookie. Then, once logged in via username/password, that cookie will be sent from their browser to the server with each subsequent request.
With that in mind, the first thing we need to do is edit the login
method, and make it assign the user's id to the session after successful login. To do so, replace the entire login method with the following:
static func login(with request: Request) throws -> Future<Response> {
return try request.content.decode(User.self).flatMap(to: Response.self) { user in
let verifier = try request.make(BCryptDigest.self)
return User.authenticate(username: user.name, password: user.password, using: verifier, on: request).unwrap(or: Abort(HTTPResponseStatus.unauthorized)).map(to: Response.self, { authedUser in
try request.session()["userId"] = "\(try authedUser.requireID())" // 1
return request.redirect(to: "/me") // 2
})
}
}
So, the only difference is here is, instead of returning JSON, we will assign the user's id to the key userId
in the session, and then redirect to the /me
route to view a very basic profile page.
Next, let's change the route to show the user's profile page. Change the contents of the route to read the following:
func getMyProfile(_ request: Request) throws -> Future<View> {
let currentUser = try request.sessionUser() // 1
let profileContext = UserProfilePage(user: currentUser) // 2
return try request.view().render("me", profileContext)
}
Here, at 1, we get the current user from the session. This is an extension (that we havent written yet..but will soon :) ), so for the moment this will throw a compile error.
At 2, we create an instance of our UserProfilePage context, to pass to the view.
Create a new class, and call it SessionAuthenticationMiddleware
, and add the following content to it:
final class SessionAuthenticationMiddleware: Middleware {
func respond(to request: Request, chainingTo next: Responder) throws -> EventLoopFuture<Response> {
let session = try request.session()
if let _ = session["userId"] { return try next.respond(to: request) }
else { throw Abort(HTTPResponseStatus.unauthorized) }
}
}
This is our middleware that we'll use to verify whether or not a session contains a userId or not. If no value is sent, then we will throw a 403 Unauthorized error. We will need to apply this middleware to all routes we wish to protect that only a valid user can view.
Create the following extension on the Request
class:
extension Request {
func sessionUser() throws -> Future<User> {
if let userId = try self.session()["userId"], let idAsInt = Int(userId) {
return User.find(idAsInt, on: self).unwrap(or: Abort.init(HTTPResponseStatus.notFound))
}
throw Abort.init(HTTPResponseStatus.unauthorized)
}
}
This is a simple extension that will allow us to call let currentUser = try request.sessionUser()
in order to conveniently retrieve the current user in our route handlers.
At this point, we have almost everything setup, we just need to use our middleware so we can protect certain routes.
At the top of the file in PostController.swift
, change the boot
method to read the following:
func boot(router: Router) throws {
let authedRoutes = router.grouped(SessionAuthenticationMiddleware()) // 1
authedRoutes.post("createPost", use: createPost) // 2
authedRoutes.get("createPost", use: showCreatePostPage) // 3
router.get("allPosts", use: viewAllPosts)
}
Here, at 1 we create a group of routes that are going to be protected by our SessionAuthenticationMiddleware. And at 2, and 3 we protect the get and post routes for both showing the createPost page, and actually creating a post. We need to protect both, not just the route for showing the createPost page, because if someone were to find our route for actually creating a post (which can be easily done by looking at the HTML form), they could still otherwise create posts via a REST api call.
Start your server, and without logging in, go to /createPost
, and you should receive a 403 Unauthorized response.
In UserController.swift
, also change the route /me
to be protected as well, like so:
let authedRoutes = router.grouped(SessionAuthenticationMiddleware())
authedRoutes.get("me", use: getMyProfile)
Next, go to the createPost
method. Currently, the userId is hardcoded (to make this compile). Now, let's use our Request extension to get the current user instead of hard coding. Replace the contents of the closure with the following:
return try request.sessionUser().flatMap(to: Post.self) { user in
let post = try Post(title: createRequest.title, userId: user.requireID())
return post.save(on: request)
}
At this point, you should be able to start your server and then login. After login, you will be redirected to a page that has your current username. You can then navigate to /createPost
to create a post, and then /allPosts
to view all posts.
The only thing missing is logout!
Log out is simple! We just need to set session()["userId"]
to be nil. So, let's add a route to logout. In UserController.swift:
boot
method:
authedRoutes.get("logout", use: logout)
In UserController
:
func logout(_ request: Request) throws -> HTTPStatus {
try request.session()["userId"] = nil
return HTTPStatus(statusCode: 200, reasonPhrase: "You are logged out")
}
Great! So now restart your server, login again, verify that you can access protected routes, and then logout. Once logged out, you shouldn't be able to access protected routes anymore.
That is all for this tutorial on web authentication using sessions :) You can find the final code for the project here. Feel free to leave any questions/comments below!