Vapor 4 Authentication: Getting Started
In this Vapor 4 tutorial, you’ll learn how to implement user authentication for your app using both bearer tokens and basic authentication headers. By Natan Rolnik.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Vapor 4 Authentication: Getting Started
30 mins
- Getting Started
- Setting up the Authentication Project
- Looking at the Project
- Running the Starter Project
- Why Authentication and Authorization Are Essential on the Server
- Authentication Mechanisms
- Adding Support for Token-Based Sessions
- Adding the Token Model
- Adding Initializers to Tokens
- Creating the Migration
- Running the Migration
- Allowing Users to Sign up
- Creating a New User
- Creating Tokens for a User
- Including the Token in the Response
- Authenticating the User With a Token
- Supporting Basic Authentication on the User Model
- Conforming a Token to the ModelTokenAuthenticatable Protocol
- Adding the Me Endpoint
- Adding the Login Endpoint
- Implementing the Login Route
- Where to Go From Here?
Creating Tokens for a User
Next, you need to make it possible to create a new token for a given user.
Below User.create(from:)
, add the following method:
// 1
func createToken(source: SessionSource) throws -> Token {
let calendar = Calendar(identifier: .gregorian)
// 2
let expiryDate = calendar.date(byAdding: .year, value: 1, to: Date())
// 3
return try Token(userId: requireID(),
//4
token: [UInt8].random(count: 16).base64, source: source,
expiresAt: expiryDate)
}
Here’s what you’re doing in this function:
- This is a throwing function (more on that in step 3) on
User
, which receives aSessionSource
and returns a newToken
. - You use a
Calendar
to generate a date a year ahead of the current date, which you’ll use as the expiry date. Change this line if you want a shorter or longer expiry date. - Using the initializer you created in the previous section, you generate the token.
requireID()
throws an error when the user isn’t in the database and doesn’t have an ID yet. - Generate the token value itself by creating 16 random bytes and getting the token’s Base 64-encoded string representation.
Including the Token in the Response
Once you’ve implemented createToken(source:)
, it’s time to activate it in the user controller.
Go back to Controllers/UserController.swift‘s create(req:)
. Add a declaration for the token below the user
property:
var token: Token!
Now, replace the .flatMapThrowing
closure after return user.save(on: req.db)
with the following:
.flatMap {
// 1
guard let newToken = try? user.createToken(source: .signup) else {
return req.eventLoop.future(error: Abort(.internalServerError))
}
// 2
token = newToken
return token.save(on: req.db)
}.flatMapThrowing {
// 3
NewSession(token: token.value, user: try user.asPublic())
}
This is what you’re doing in this chunk of code:
- You create a new token from the fresh user you just saved. If it fails, it throws an error.
- You keep a reference to the token and save it.
- Upon completion, you initialize the
NewSession
with thetoken
value and theUser.Public
from theuser
. TheUser.Public
struct is a technique used to send information to the clients without exposing fields that are internal to the server.
It’s finally time to test the first API you created with this tutorial!
Build and run, then open the Paw or Postman API file from this tutorial’s download materials. Select the (1) Sign up request, set the server URL to localhost:8080, then send the request.
You’ll get a JSON response that includes both the token and the user:
{
"token": "t+oHBUwU2Rv+qp7O2Ed0UQ==",
"user": {
"username": "NatanTheChef",
"id": "138191B9-445D-442D-9F70-B858081A661B",
"updated_at": "2020-03-07T19:40:54Z",
"created_at": "2020-03-07T19:40:54Z"
}
}
token
and id
values will be different.Congratulations! You’ve created a new user and token, and returned that token in your server response. Next, you’ll use that token to authenticate the user.
Authenticating the User With a Token
There are two ways to confirm the server saved the user: The first is to use the SQLite browser by opening the users table to see the new record — select the users table, then select the Browse Data tab. The second is to add an endpoint allowing users to fetch their own information.
Your next step will be to add that endpoint.
Supporting Basic Authentication on the User Model
In order for Vapor to know that a User
instance can be used for authentication, you need to conform User
to ModelAuthenticatable
. Vapor uses this protocol to perform all the steps around authenticating a user with a username and a password in the request headers and to link it to tokens.
Open User.swift and add the following extension:
extension User: ModelAuthenticatable {
// 1
static let usernameKey = \User.$username
static let passwordHashKey = \User.$passwordHash
// 2
func verify(password: String) throws -> Bool {
try Bcrypt.verify(password, created: self.passwordHash)
}
}
Fluent’s ModelAuthenticatable
protocol is succinct. First, it requires two KeyPath
s, where the value is a field of type String
. This tells Vapor which fields to look for when querying the user upon authentication.
Secondly, it requires your user model to implement verify(password:)
, which determines whether the received password matches the password hash and returns the result. As with the registration, this function also uses Bcrypt
to perform this check.
Conforming a Token to the ModelTokenAuthenticatable Protocol
Next, Vapor needs to know that it can use the Token
for authenticating users. This lets it provide the authentication middleware, which is able to find the user for a token supplied in the request authentication header.
Open Models/Token.swift and add this extension:
extension Token: ModelTokenAuthenticatable {
//1
static let valueKey = \Token.$value
static let userKey = \Token.$user
//2
var isValid: Bool {
guard let expiryDate = expiresAt else {
return true
}
return expiryDate > Date()
}
}
Fluent’s ModelTokenAuthenticatable
protocol is also very concise. First, it needs two KeyPath
s — one for the token value field and another for the user relationship. Next, it checks the token to see if it’s valid at a specific moment. If your tokens don’t expire, simply return true
.
In this case, you take expiresAt
and compare it to the current date. If the current date is later than the expiry date, it’s invalid, causing Vapor to delete the token from the database and return 401 unauthorized
.
Adding the Me Endpoint
Now Vapor can provide the token authenticator middleware. Go back to UserController.swift and add the following lines to the end of boot(routes:)
:
let tokenProtected = usersRoute.grouped(Token.authenticator())
tokenProtected.get("me", use: getMyOwnUser)
The first line creates a router using the /users
path defined in the first line of this method, and also wraps them in an authentication middleware. This means that every request going through the middleware requires an authenticated user.
The second line makes the /users/me
endpoint use getMyOwnUser(req:)
. Scroll down to this function and replace the thrown error with the following line:
try req.auth.require(User.self).asPublic()
This returns the user information by accessing the request’s authentication cache, fetches the user who’s performing the request and converts it to a public user. If the request isn’t authenticated, then it throws a 401 unauthorized
error.
Whenever you need to get the current user from within a request, simply use req.auth.require(User.self)
, as long as the request came through an authentication middleware, as shown above.
Build and run, then send the (2) Me request in the API file. The response should contain the user object you just signed up:
{
"username": "NatanTheChef",
"id": "138191B9-445D-442D-9F70-B858081A661B",
"updated_at": "2020-03-07T19:40:54Z",
"created_at": "2020-03-07T19:40:54Z"
}