JWT Access and Refresh Token with Vapor 3

JWT Access and Refresh Token with Vapor 3Timur ShafigullinBlockedUnblockFollowFollowingJan 30IntroductionWe often need to identify the user who sends a request to the server, and we also need to ensure the security of the transmission of client-server information.

This is where access tokens help us, in particular, JWT.

JSON Web Token (JWT) is an open standard (RFC 7519) for creating access tokens based on JSON format.

As a rule, it is used to transfer authorization data in client-server applications.

Tokens are created by the server, signed by the secret key and transferred to the client, who later uses this token to confirm his identity.

For full security, our JWT should have a limited lifetime, and for its updating, it is necessary that the user can confirm his authenticity.

In this article, we will look at how we can do this without storing user data on the client.

We will not go into details about JWT because it is out the scope of this article.

I recommend reading this article to familiarize yourself with JWT.

Authentication and AuthorizationTo begin with, let’s highlight two concepts: authentication and authorization.

Authentication it is the verification of user credentials (usually a login/password pair).

Authorization — giving a certain people or groups of persons the right to perform certain actions, that is, the procedure for checking whether a user has access to a particular resource.

Typically, the authentication and authorization process is as follows:The user sends his data to the server (login/password).

Authentication process.

In the case of correct data, the server generates a JWT token for the user with the necessary data inside (permissions, ID, token lifetime).

The user sends a request to the server with the received token.

The server decides whether to give access to the user based on the data in the token (permissions, lifetime).

The authorization process.

Refresh TokenIn order not to store user data and still be able to update JWT, we will be helped by a new concept, such as Refresh Token.

The authentication token will be called Access Token.

The second token to update Access Token is Refresh Token.

We will keep it on the client, instead of the login/password pair.

As a rule, Refresh Token lives much longer than Access Token, for example, a month or two.

In one of my projects, the Refresh Token lifetime is 60 days.

We will store the Refresh Token in the database and verify it as soon as the client wants to update his Access Token.

It is a randomly generated string itself (for example, Lxd6bj7w33GEX1GOSgzCNZWNSMskaUmPwgG6uM).

Let’s see the update algorithm in more detail.

Access Token updateThe client is authenticated by sending a login/password pair.

If successful, the server creates a new Access Token and Refresh Token.

Refresh Token is recorded in the database along with the time of his life.

The server sends the response to the client in the form:{ "expired_at": .

, "access_token": .

, "refresh_token": .

}4.

The client saves this data.

5.

With each request, the client checks to see if Access Token is expired or not.

In case of Access Token has not expired, it simply sends the request using it.

6.

For updates, the client sends a Refresh Token to a specially defined endpoint.

(for example /v1/account/refresh-token)7.

The server checks the Refresh Token, which is in the database with the sent.

Also, check expired or not.

8.

In case of Refresh Token is expired, or it is not found in the database, then we return a 401 error to the client, in order to pass authentication.

9.

If Refresh Token is found and it is still relevant, then create a new Access Token and update Refresh Token.

We send this data as in paragraph 3.

10.

Now the client can continue to perform requests with the new Access Token.

ImplementationConsider a specific implementation on the Vapor 3.

Project SetupI created and set up a starter project.

Download it here.

Project StructureFolder Config contains the project settings.

Launch folder has standard Vapor files.

The ProjectServices.

swift contains instances of the service classes used in the project.

The Controllers folder contains the controllers themselves, which are called upon requests.

There are two ORM SQLite database models in the project: Todo and User.

The idea will be that the user can register, and after authentication will receive Access and Refresh Token.

With this token, the user can create, get or delete his TODOs.

User and Todo have a one-to-many relationship.

That is, one user may have several Todo notes.

In the DTO (Data Transfer Object) folder contains the models for transferring data to the response body.

They should not contain any business logic.

The ResponseDto model only to respond with any message.

In the Services folder, we store all of our services, which contain the main business logic.

Create JWT Access TokenThe next step we need is that after authentication, the user can get their Access Token.

To do this, let’s create the Tools folder in the App folder.

In the Tools folder itself, create another JWT folder.

The result is the following structure:Creating a JWT folderCreate a file JWTConfig.

swift, its contents will be as follows:Here is the basic configuration of our JWT Access Token.

We also need to handle errors related to JWT.

For this, we will use the already existing class JWTError.

Write an extension for it in a new file JWTErrorExtensions.

swift:Create an AccessTokenPayload.

swift file with the following contents:The payload is JWT public parameters that will be encoded in base64.

Also, JWTPayload requires implementing the verify(signer:)method, which throws an exception if our token is no longer valid.

In our case, we only check if the token has expired (you can also check if the issuer is correct or not).

To work with JWT, create a separate class — TokenHelpers:With this helper, we can create a new JWT Access Token, find out the expiring time of the token, verify and get the userID.

To send a new token, create an AccessDto model in the Models/DTO folder:Great, now we have everything ready to create a new token.

Add the signIn(request:, user:) function to DefaultUserService.

swift:We are looking for a user by login and if successful, we check our hashed password with a special function digest.

verify(_, created:) and then we issue a new Access Token and the time of its expiration to our client.

Using Access TokenIn order for each request in the controller not to check every time a valid token or not, we will create our own Middleware.

From official documentation:Middleware is placed between the server and your router.

It is capable of mutating both incoming requests and outgoing responses.

Middleware can choose to pass requests on to the next Middleware in a chain, or they can short circuit and return a custom `Response` if desired.

Before the request reaches the controller, we verify that the token in the header is valid, otherwise, we simply return an error, letting the client know that the token is no longer valid.

In the JWT folder, create a JWTMiddleware.

swift file:Everything is simple: we check the availability of the token and verify it (checking if the token has expired).

In case of an error, we return to the client an error with the description.

An extension Request to get a token and an authorized user from it will also be useful (you can put it in Tools/Extensions):Now, let’s create everything necessary so that the user can create, get or delete a Todo note.

To do this, create TodoDto.

swift (Models/DTO):Create a service that will contain our business logic for working with Todo notes and cover it with the protocol (Services/TodoService):Update TodoController.

swift:Note the creation of a router: let group = router.

grouped("v1/todo").

grouped(JWTMiddleware()).

Here we specify the common base path of the endpoint and also add JWTMiddleware, that is, all requests of this group will pass the verification of the token.

Add our new service to ProjectServices.

swift:And update routes.

swift:Great, now we can build a project, and check that everything works!We pass authentication (v1/account/sign-in), in response, we get our Access Token (if there is no user, you can create it using endpoint /v1/account/sign-up):Copy and paste our token in the Authorization header:After that, we can already create, get or delete Todo notes from an authorized user:Great, but after a while, you get an error:{ "error": true, "reason": "JWT verification failed"}Unfortunately, our Access Token expired, and we no longer have access to our Todo notes (I remind you that you can set a longer Access Token lifetime, for this lesson this time is only 100 seconds).

For this, we need to re-pass the authentication process.

Wait, no, no, we also have… Refresh Token!Creating Refresh TokenAdd a new function for this to the TokenHelpers class:The function is as simple as possible: it generates a string of length 40 with random characters from the variable letters.

Now you need to create a model in which we will store the Refresh Token of the user (one user may have several Refresh Token):Add a relation to the User model:Great, now at login we need to create and give to the client Access and Refresh Token.

Update our AccessDto:Added new field “refreshToken”Well, now our signIn function will look like this (added Refresh Token to the response and save it in the database):Do not forget to add the model migration to configure.

swift:migrations.

add(model: RefreshToken.

self, database: .

sqlite)Now sign-in can return Refresh Token!.It remains to learn how to use it to update Access Token.

Using Refresh TokenAdd a new function to update our tokens:Everything is simple — we are looking for our model with Refresh Token in the database, otherwise, we return the status unauthorized (in order to notify the client that it is necessary to pass authentication again).

Next, we check that our Refresh Token is not expired and if successful, create a new Access and update Refresh Token in the database (do not forget about the date either).

If our Refresh Token is already expired, then remove it from the database and return the status to the client unauthorized.

Do not forget to add a new method to the protocol:protocol UserService {.

func refreshToken(request: Request, refreshTokenDto: RefreshTokenDto) throws -> Future<AccessDto>}Add a new function to the UserController:func refreshToken(_ requets: Request, refreshTokenDto: RefreshTokenDto) throws -> Future<AccessDto> { return try self.

userService.

refreshToken(request: requets, refreshTokenDto: refreshTokenDto)}Register endpoint to update the token in the boot function:func boot(router: Router) throws { let group = router.

grouped("v1/account") .

group.

post(RefreshTokenDto.

self, at: "/refresh-token", use: self.

refreshToken)}After authenticating, we save our received Refresh Token from the JSON response:{ "accessToken": ".

", "refreshToken": "AS3GZB6z59YVoJ0CLOKVzWWktxNE6SBAgTMr1K86l", "expiredAt": ".

"}Now, after our Access Token is expired, we can update it using Refresh Token on endpoint /v1/account/refresh-token:Perfectly!.Now we can update our Access Token without storing user data!ConclusionThis scheme of using Access + Refresh Token guarantees that an attacker will not be able to find out the user’s data.

Also, for greater security, you can make a blacklist of tokens that are blocked or stolen.

And to optimize the storage periodically clean expired Refresh Token from the database.

That’s all, if you have any additions or suggestions, I am always open for discussion, do not hesitate to ask in the comments!The final version of the project can be found here: https://github.

com/timbaev/TokensTutorialFinal.

. More details

Leave a Reply