Tutorial: How to build Basic Auth

This tutorial will teach you how to authenticate with your backend using basic auth. It is the simplest technique to secure web resources because it does not require cookies or session identifiers. I think it is perfect to start off with the topic authentication when having a frontend like iOS / VueJS / Android etc. and you aren’t yet ready to dive into a more advanced one like Bearer auth 😊

You can find the result of this tutorial on github here

This tutorial is a natural follow-up of How to write Controllers. You can either go for that tutorial first and come back later or be a rebel, skip it and read on 😊


Index

1. Create a new project
2. Generate Xcode project
3. Register AuthenticationProvider
4. Adjust Model: User
5. The REGISTER route
6. The LOGIN route
7. The PROFILE route
8. Where to go from here


1. Create and generate a new project

We will use the outcome of the aforementioned tutorial as a template to create our new project:

vapor new projectName --template=vaporberlin/my-first-controller

2. Generate Xcode project

Before we generate an Xcode project we would have to change the package name within Package.swift and remove one dependency that we wont need:

// swift-tools-version:4.0
import PackageDescription
let package = Package(
name: "projectName", // changed
dependencies: [
.package(url: "https://github.com/vapor/vapor.git", from: "3.0.0-rc"),
.package(url: "https://github.com/vapor/leaf.git", from: "3.0.0-rc"),
.package(url: "https://github.com/vapor/fluent-sqlite.git", from: "3.0.0-rc"),
.package(url: "https://github.com/vapor/auth.git", from: "2.0.0-rc") // added
],
targets: [
.target(name: "App", dependencies: ["Vapor", "Leaf", "FluentSQLite", "Authentication"]), // added
.target(name: "Run", dependencies: ["App"]),
.testTarget(name: "AppTests", dependencies: ["App"]),
]
)

Now in the terminal at the root directory projectName/ execute:

vapor update -y

It may take a bit fetching the dependency, but when done you should have a project structure like this:

projectName/
├── Package.swift
├── Sources/
│ ├── App/
│ │ ├── Controllers/
│ │ │ └── UserController.swift
│ │ ├── Models/
│ │ │ └── User.swift
│ │ ├── app.swift
│ │ ├── boot.swift
│ │ ├── configure.swift
│ │ └── routes.swift
│ └── Run/
│ └── main.swift
├── Tests/
├── Resources/
│ └── Views/
│ └── userview.leaf
├── Public/
├── Dependencies/
└── Products/

3. Register AuthenticationProvider

Go to our configure.swift and add the following:

import Vapor
import Leaf
import FluentSQLite
import Authentication // added
public func configure(
_ config: inout Config,
_ env: inout Environment,
_ services: inout Services
) throws {
  ...
  try services.register(AuthenticationProvider())
  ...
}

4. Adjust Model: User

Since we are going to have the Authentication library do all the work for us regarding authentication, all we have to do is let it know what the credentials for the authentication are. And so in our Models/User.swift we will adjust our User and extend it by BasicAuthenticatable:

import FluentSQLite
import Vapor
import Authentication // added
final class User: SQLiteModel {
var id: Int?
var email: String // added
var password: String // added
init(id: Int? = nil, email: String, password: String) {
self.id = id
self.email = email
self.password = password
}
}
extension User: BasicAuthenticatable {
static var usernameKey: WritableKeyPath<User, String> {
return \.email
}
  static var passwordKey: WritableKeyPath<User, String> {
return \.password
}
}
extension User: Content {}
extension User: Migration {}

We have removed the username property and introduced an email property. Conforming to the BasicAuthenticatable protocol asks us to tell where to find the usernameKey and where to find the passwordKey and really a usernameKey could be an email, an actual username or even a phone number if you want your user to register with that instead 😊


5. The REGISTER route

In our Controller/UserController.swift adjust the code to the following:

import Vapor
import Crypto
final class UserController {

func register(_ req: Request) throws -> Future<User.Public> {
return try req.content.decode(User.self).flatMap { user in
let hasher = try req.make(BCryptDigest.self)
let passwordHashed = try hasher.hash(user.password)
let newUser = User(email: user.email, password: passwordHashed)
      return newUser.save(on: req).map { storedUser in
return User.Public(
id: try storedUser.requireID(),
email: storedUser.email
)
}
}
}
}

Now you can see we return an instance of User.Public and this is just another struct within Models/User.swift:

import FluentSQLite
import Vapor
import Authentication
final class User: SQLiteModel {
...
}
extension User: BasicAuthenticatable {
...
}
extension User {
struct Public: Content {
let id: Int
let email: String
}
}
extension User: Content {}
extension User: Migration {}

And we do that because we don’t want to return an instance of a user since that one would include the hashed password as well. And to do that might not be as secure as it sounds. Well it never did sound secure, right 😄?

Also inside routes.swift adjust the code to:

import Vapor
public func routes(_ router: Router) throws {
  let userController = UserController()
router.post("register", use: userController.register)

}

In order to make a POST-request (we defined that for our register() function within routes.swift) we’ll need a tool like Postman or Paw (if you prefer a GUI). But you could also just use your Terminal with a native command called curl. And that’s what we’ll go with. 🤓

5.1 CURL — Overview
First an overview on how the curl command for our POST-request looks like:

curl -H "Content-Type: application/json" -X POST -d '{"email":"zelda@hyrule.com", "password": "myheroislink"}' http://localhost:8080/register

There are only four parts here. It’s really easier than it looks!
• With -H we are setting a HEADER with “Content-Type: application/json”
• With -X we are defining the HTTP-Method we want to fire (POST here)
• With -d we are saying that next follows our data we want to send
• A curl command always ends with the url you want to fire the request to

The reason we have to define what type of data (JSON) we want to send with curl is because curl is sending data form-encoded by default. That is the same encoding used when you submit a form on a website ☝🏻🤓. That’s why we need to define the type of data within the header with -H (just as we do) 😊.

Now cmd+r or run your project and check your Xcode Console for the port and let’s fire our first curl request in our terminal to create a user:

curl -H "Content-Type: application/json" -X POST -d '{"email":"zelda@hyrule.com", "password": "myheroislink"}' http://localhost:8080/register

It should return:

{"id":1,"email":"zelda@hyrule.com"}

6. The LOGIN route

Now basic authentication requires you to send the credentials base64 encoded in the header of your request. And since you’d have to do that for each request to an endpoint that is secured there is no session and basically no real “login” (I mean what would it do?) But we will create a login endpoint anyways and use it to just check whether the credentials are valid or not. 😊

In our routes.swift add the following code:

import Vapor
import Crypto // added
public func routes(_ router: Router) throws {
let userController = UserController()
router.post("register", use: userController.register)
  let middleWare = User.basicAuthMiddleware(using: BCryptDigest())
let authedGroup = router.grouped(middleWare)
authedGroup.post("login", use: userController.login)

}

Since we conformed our User entity to BasicAuthenticatable it got a function called basicAuthMiddleware into which we can pass our hasher that we use to hash the password when saving it to the database ☝🏻🤓.

Now this is the first half of securing a route and now to the second half in where you add the following code in Controller/UserController.swift:

import Vapor
import Crypto
final class UserController {
  func register(_ req: Request) throws -> Future<User.Public> {
...
}

func login(_ req: Request) throws -> User.Public {
let user = try req.requireAuthenticated(User.self)
return User.Public(id: try user.requireID(), email: user.email)
}

}

And so if this function is executed it will return unauthorized if you don’t provide base64 encoded credentials of a user in the header of your request. Because requireAuthenticated(User.self) tries to get it but can’t find it. Now how do we do that using our terminal? I’m glad you asked young padawan.

NOTE: Since you probably re-run your project to compile the new code make sure you register (create) the user again we now are about to use for authentication 🤓

So we will want to base64 encode our credentials in the following form:

“username:password”

And this is how the command looks like (no installation whatsoever needed):

echo -n "zelda@hyrule.com:myheroislink" | base64

The -n is to avoid a newline character at the end of our string. If we now hit enter we get a base64 encoded result that ends with an “= like:

emVsZGFAaHlydWxlLmNvbTpteWhlcm9pc2xpbms=

Let us first try to access the login route without providing credentials:

curl -X POST http://localhost:8080/login

This will return:

{"error":true,"reason":"User has not been authenticated."}

And that’s cool! It means it works! Let’s try the same but with credentials:

curl -H "Authorization: Basic emVsZGFAaHlydWxlLmNvbTpteWhlcm9pc2xpbms=" -X POST http://localhost:8080/login

This will return:

{"id":1,"email":"zelda@hyrule.com"}

Having this we now know the credentials are valid and we can save them in the frontend to then use them when trying to access any other secured route!

NOTE: Since there’s no real login there won’t be a real logout. You can still provide a logout-button in the frontend to then for example delete the credentials you stored locally 😊.

7. The PROFILE route

In our Controller/UserController.swift adjust the code to the following:

import Vapor
import Crypto
final class UserController {
  func register(_ req: Request) throws -> Future<User.Public> {
...
}

func login(_ req: Request) throws -> User.Public {
...
}
  func profile(_ req: Request) throws -> String {
let user = try req.requireAuthenticated(User.self)
return "You're viewing \(user.email) profile."
}
}

Also inside routes.swift adjust the code to:

import Vapor
import Crypto
public func routes(_ router: Router) throws {
let userController = UserController()
router.post("register", use: userController.register)
  let middleWare = User.basicAuthMiddleware(using: BCryptDigest())
let authedGroup = router.grouped(middleWare)
  authedGroup.post("login", use: userController.login)
authedGroup.get("profile", use: userController.profile) // added
}

Now cmd+r or run your project and remember that we are using an in memory database, that means all our data will be wiped after we re-run our project. You will have to again register a new user and then create a base64 encoded string of the credentials and then try to access the profile route. It will return “You’re viewing zelda@hyrule.com profile.”

This is a GET request so your curl command should look like:

curl -H “Authorization: Basic emVsZGFAaHlydWxlLmNvbTpteWhlcm9pc2xpbms=” http://localhost:8080/profile

That’s it!! You successfully implemented basic auth 🎉🚀✨


8. Where to go from here

You can find a list of all tutorials with example projects on Github here:
👉🏻 https://github.com/vaporberlin/vaporschool


I am really happy you read my article! If you have any suggestions or improvements of any kind let me know! I’d love to hear from you! 😊

Like what you read? Give Martin Lasek a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.