Tutorial: How to build Bearer Auth
This tutorial will teach you how to authenticate with your backend using bearer authentication (also called token authentication). It is a cryptic string generated by the server in response to a login request. It wins over Basic Auth in terms of security since you don’t have to send base64 encoded credentials in each of your request but instead a frontend like iOS / Android / VueJS etc. sends this cryptic token 😊
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. Add Model: Token
5. Adjust Model: User
6. The REGISTER route
7. The LOGIN route
8. The PROFILE route
9. BONUS: Logout
10. 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 PackageDescriptionlet package = Package(
name: "projectName", // changed
dependencies: [
.package(url: "https://github.com/vapor/vapor.git", from: "3.0.0"),
.package(url: "https://github.com/vapor/leaf.git", from: "3.0.0"),
.package(url: "https://github.com/vapor/fluent-sqlite.git", from: "3.0.0"),
.package(url: "https://github.com/vapor/auth.git", from: "2.0.0") // 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 // addedpublic func configure(
_ config: inout Config,
_ env: inout Environment,
_ services: inout Services
) throws { ... try services.register(AuthenticationProvider()) ...
}
4. Add Model: Token
When a user successfully logs in, we will be creating a new token with a relation to that user and store it in the database. That way whenever a request to a protected route is made, we will be able to look up whether the token in the request header exists and use its relation to get the according user. Within Models/ create a new swift file called Token.swift 😊
import FluentSQLitefinal class Token: SQLiteModel {
var id: Int?
var token: String
var userId: User.ID
init(token: String, userId: User.ID) {
self.token = token
self.userId = userId
}
}extension Token {
var user: Parent<Token, User> {
return parent(\.userId)
}
}
We created a simple Token-Model that stores a string but also the relation to a user with a convenience method in the extension to retrieve him.
NOTE: You can learn more about relations here: one-to-many-relations 😊
Conform our Token Model to BearerAuthenticatable and let it know what property of our Model the actual token is. To do so add the following:
import FluentSQLite
import Authentication // addedfinal class Token: SQLiteModel {
...
}extension Token {
...
}extension Token: BearerAuthenticatable {
static var tokenKey: WritableKeyPath<Token, String> { return \Token.token }
}
Next we have conform our Model to the Authentication.Token protocol and we use the Librarie’s namespace here because our Model is also called Token and the compiler wouldn’t know whether we mean the Token protocol or our own Token class when extending 😊
import FluentSQLite
import Authenticationfinal class Token: SQLiteModel {
...
}extension Token {
...
}extension Token: BearerAuthenticatable {
...
}extension Token: Authentication.Token {
typealias UserType = User
typealias UserIDType = User.ID static var userIDKey: WritableKeyPath<Token, User.ID> {
return \Token.userId
}
}
First we assign our Class-Type that owns the token to the UserType which is User in our case. In most cases you would have a User to whom the token shall be related to. Next we assign the type of the id of the same class that we assigned to UserType (ID here is an alias for Int). Lastly we tell the library at what property of our Token Model we hold the user id for the relation.
Finally we will conform our Token to Migration and to Content:
import FluentSQLite
import Authenticationfinal class Token: SQLiteModel {
...
}extension Token {
...
}extension Token: BearerAuthenticatable {
...
}extension Token: Authentication.Token {
...
}extension Token: Migration {} // added
extension Token: Content {} // added
And add it to our configure.swift:
import Vapor
import Leaf
import FluentSQLite
import Authenticationpublic func configure(
_ config: inout Config,
_ env: inout Environment,
_ services: inout Services
) throws {
... var migrations = MigrationConfig()
migrations.add(model: User.self, database: .sqlite)
migrations.add(model: Token.self, database: .sqlite) // added
services.register(migrations)
}
5. Adjust Model: User
In our User Model we need to do a small adjustment and conform to the protocol TokenAuthenticatable so in Models/User.swift add the following.
import FluentSQLite
import Vapor
import Authentication // addedfinal class User: SQLiteModel {
...
}extension User: Content {}
extension User: Migration {}extension User: TokenAuthenticatable {
typealias TokenType = Token
}
Looking at the protocol we can see that we get two things out of it. The first thing is we give away what our token type is: Token. The second thing is, out User class now knows the function authenticate which needs the token type.
6. The REGISTER route
In our Controller/UserController.swift delete everything that was in there and add the following code:
import Vapor
import Crypto // addedfinal 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.UserPublic(
id: try storedUser.requireID(),
email: storedUser.email
)
}
}
}
}
I think most of it should be relatively self explanatory 🤞🏻 Now you can see we return an instance of User.UserPublic and this is just another struct within Models/User.swift:
import FluentSQLite
import Vapor
import Authenticationfinal class User: SQLiteModel {
...
}extension User: Content {}
extension User: Migration {}extension User: TokenAuthenticatable {
typealias TokenType = Token
}extension User {
struct UserPublic: Content {
let id: Int
let email: String
}
}
And we do that because we don’t want to return an instance of the User class itself since that one includes the hashed password. And that is not as secure as it sounds. Well actually it doesn’t sound secure.. at all 😄 So.. that’s why 😊!
Don’t forget to adjust routes.swift to the following:
import Vaporpublic 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 here. 🤓
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": "link"}' 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 to send JSON. 😊
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": "link"}' http://localhost:8080/register
It should return:
{
"id":1,
"email": "zelda@hyrule.com",
}
7. The LOGIN route
Now bearer authentication requires you to send the token in the header of your request for each secured route. Our login route however is not secured here we will check whether the provided user (as JSON) exists and if yes, it will create a token and return it. You can then use that token to access secured routes.
In our UserController.swift add the following code:
import Vapor
import Crypto
import Random // added
import FluentSQLite // addedfinal class UserController { func register(_ req: Request) throws -> Future<User.Public> {
...
} func login(_ req: Request) throws -> Future<Token> {
return try req.content.decode(User.self).flatMap { user in
return User.query(on: req).filter(\.email == user.email).first().flatMap { fetchedUser in
guard let existingUser = fetchedUser else {
throw Abort(HTTPStatus.notFound)
} let hasher = try req.make(BCryptDigest.self)
if try hasher.verify(user.password, created: existingUser.password) {
let tokenString = try URandom().generateData(count: 32).base64EncodedString()
let token = try Token(token: tokenString, userId: existingUser.requireID())
return token.save(on: req)
} else {
throw Abort(HTTPStatus.unauthorized)
}
}
}
}
}
In the beginning of 7. The LOGIN route I already mentioned what we wanted to achieve and we basically just did that. We are decoding the received JSON into a user instance called user here. And we use the email of that user to look up if we have a user with such an email in our database. If the fetched user is nil we know a user with the provided email doesn’t exist and throw notFound. Whereas if he exists we let hasher do the work to verify if the password of the user matches the password of the existingUser. If it it’s true we can go create a token, store it with the users id in the database and return both. If it’s false we simply throw unauthorized.
Let’s not forget to call our login function within routes.swift:
import Vaporpublic func routes(_ router: Router) throws {
let userController = UserController()
router.post("register", use: userController.register)
router.post("login", use: userController.login) // added
}
8. The PROFILE route
Now that we are able to register ourselves and then retrieve the token when we login I think it’s time to create a secured route. How about a profile 😏?
Let’s go to routes.swift and add the following:
import Vaporpublic func routes(_ router: Router) throws {
let userController = UserController()
router.post("register", use: userController.register)
router.post("login", use: userController.login) let tokenAuthenticationMiddleware = User.tokenAuthMiddleware()
let authedRoutes = router.grouped(tokenAuthenticationMiddleware)
authedRoutes.get("profile", use: userController.profile)
}
When the url profile is tried to be accessed the tokenAuthMiddleware will check whether a token can be found in the request header. It will then try to fetch the related user from our database and pass him in to the authenticate function on the current request. That stores the user in the request cache so then we can access him in our controller function in UserController.swift:
import Vapor
import Crypto
import Random
import FluentSQLitefinal class UserController { func register(_ req: Request) throws -> Future<User.Public> {
...
} func login(_ req: Request) throws -> Future<Token> {
...
} func profile(_ req: Request) throws -> Future<String> {
let user = try req.requireAuthenticated(User.self)
return req.future("Welcome \(user.email)")
}
}
Now the function requireAuthenticated(…) will give us a user instance that is in the cache of our request. No database interaction needed since the middleware did the job for us already 😊
Okay so now we will compile/run our code and go through the whole process! First fire a curl request to register a user:
curl -H "Content-Type: application/json" -X POST -d '{"email":"zelda@hyrule.com", "password": "link"}' http://localhost:8080/register
You should get something like the following returned:
{
"id": 1,
"email": "zelda@hyrule.com",
}
Next we’ll use the same credentials we used for registering the user to login:
curl -H "Content-Type: application/json" -X POST -d '{"email":"zelda@hyrule.com", "password": "link"}' http://localhost:8080/login
As a result if we login successfully we should get:
{
"id": 6,
"token": "4DXuk0Ucg9/t0V5c22G9cuCSq9cEk34eqXVmyf9St4k=",
"userId":1
}
Finally we will use the token to access the /profile route:
curl -H "Authorization: Bearer 4DXuk0Ucg9/t0V5c22G9cuCSq9cEk34eqXVmyf9St4k=" http://localhost:8080/profile
NOTE: in case the random string generates a slash somewhere in between: “/”
You will receive it escaped like: ”\ /” and in that case before you send it you will have to remove the backslash: \ and only keep the normal / in your token 👌🏻
And as a result we get…
Welcome zelda@hyrule.com
That’s it!! You successfully implemented bearer auth 🎉🚀✨
9. Bonus: Logout
Now you must have noticed that after every login with the same user you would get a different (a new) token. Which is totally fine and makes sense! However since we are not deleting the old ones your table would grow massively after a certain time 😅 Let us fix that together!
First let us adjust our login function in our UserController.swift:
import Vapor
import Crypto
import Random
import FluentSQLitefinal class UserController { func register(_ req: Request) throws -> Future<User.UserPublic> {
...
} func login(_ req: Request) throws -> Future<Token> {
return try req.content.decode(User.self).flatMap { user in
return User.query(on: req).filter(\.email == user.email).first().flatMap { fetchedUser in
guard let existingUser = fetchedUser else {
throw Abort(HTTPStatus.notFound)
} let hasher = try req.make(BCryptDigest.self)
if try hasher.verify(user.password, created: existingUser.password) { return try Token
.query(on: req)
.filter(\Token.userId, .equal, existingUser.requireID())
.delete()
.flatMap { _ in
let tokenString = try URandom().generateData(count: 32).base64EncodedString()
let token = try Token(token: tokenString, userId: existingUser.requireID()) return token.save(on: req)
}
} else {
throw Abort(HTTPStatus.unauthorized)
}
}
}
} func profile(_ req: Request) throws -> Future<String> {
...
}
}
Now here after we made sure a user exists and the provided password is valid we can go and delete all tokens that are related to this user before we go and create a new one 👍🏻
Next we’ll create a logout function that simply deletes all user related tokens:
import Vapor
import Crypto
import Random
import FluentSQLitefinal class UserController { func register(_ req: Request) throws -> Future<User.UserPublic> {
...
} func login(_ req: Request) throws -> Future<Token> {
...
} func profile(_ req: Request) throws -> Future<String> {
...
} func logout(_ req: Request) throws -> Future<HTTPResponse> {
let user = try req.requireAuthenticated(User.self)
return try Token
.query(on: req)
.filter(\Token.userId, .equal, user.requireID())
.delete()
.transform(to: HTTPResponse(status: .ok))
}
}
Last step is to add a new route and have it be a secured one because we want the middleware to fetch the user based on the provided token and cache him in our request so we can get him within our controller function (like we do) to find all related tokens to then delete them. So in routes.swift add:
import Vaporpublic func routes(_ router: Router) throws { let userController = UserController()
router.post("register", use: userController.register)
router.post("login", use: userController.login) let tokenAuthenticationMiddleware = User.tokenAuthMiddleware()
let authedRoutes = router.grouped(tokenAuthenticationMiddleware)
authedRoutes.get("profile", use: userController.profile)
authedRoutes.get("logout", use: userController.logout)
}
10. Where to go from here
You can find a list of all tutorials with example projects on Github here:
👉🏻 https://github.com/vaporberlin/vaporschool