Web Authentication from Scratch Using Sessions

1227 Views

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 :)

Current Setup

Right now, the starter project has the following:

  • There is a User class and a Post class.
  • Each post belongs to one user
  • The User class conforms to PasswordAuthenticatable
  • There are routes/views for creating a post, registering a user, and viewing all posts
  • I am using a Postgres db
  • I have added the Authentication, Leaf, and PostgreSQL providers

Roadmap

The general flow for setting up session auth in our app is the following:

  • Enable Sessions in our app
  • After valid username/password login, assign userId to session cookie (session()["userId"]) for unique session identification.
  • Create middleware that can verify whether or not a request has a userId associated with its session
  • Apply this middleware to routes we want to protect. Only valid sessions with a userId can access them.
  • Set session()["userId"] to nil to logout
With that in mind, let's get started!

Setup App for Using Sessions

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.

Setup the Database (Postgres only)

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 

Assign userId to Session

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.

Edit the /me Route

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.

Add the Middleware and Request Extension

Middleware

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.

Request Extension

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.

Add the Middleware to our Routes

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!

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!



Enjoy this article? Consider supporting VaporForums by following us on Twitter! Get the latest Vapor articles, tutorials, and news.