Authentication with JWT in Dart

How to implement JSON Web Token Authentication in Dart

Kenneth Reilly
ITNEXT

--

Introduction

With security concerns steadily on the rise, organizations are searching for better and faster means of implementing strong and reliable security measures to safeguard critical infrastructure and important customer data.

In this article, we’ll look at how to get basic authentication up and running in Dart with JSON Web Tokens, an increasingly popular and easy-to-use method of securing authentication between two machines over a network.

Dart is an expressive and efficient language, suitable for handling any standard client or server task, which makes it a great choice for building end-to-end client-server solutions based on modern concepts such as the SOLID design principles and the twelve-factor app methodology.

The source code for the example project is available here on GitHub. In case you don’t already have a Dart environment, you can get it here.

Project Setup

This project was created with the command$ stagehand package-shelfwhich initializes a boilerplate project for a shelf web server.

First, let’s check out the project definition in pubspec.yaml:

Within this file are standard definitions for name, description, environment, and dependencies. The jaguar_jwt package is added to handle the internal JWT tasks of creating and validating tokens. The dotenv package was also added for compliance with Factor III (Config) of the 12-factor principles, and to allow the use of local environment variables for prototyping and testing.

A default config file .env.example was created for use with this project:

The PORT variable indicates that the server will run on port 3000, while JWT_AUTH_SECRET is an application-specific secret that is used to generate tokens, and validate them to ensure they originated from our network.

Application Entry Point

The next file we’ll take a look at is bin/server.dart:

The Server class handles initialization of the example API server, including the environment variable loading, command-line argument parsing, and server pipeline/middleware setup. The debug mode defined as an argument is not implemented in this example, and is provided for illustration only.

The _echo method is a general-purpose HTTP request handler that will always return OK, making it a great test point to work from, ensuring that whether or not we get an OK is completely dependent upon the underlying middleware.

The auth middleware is created using the aptly-named createMiddleware, and then placed in the pipeline after request logging and before the _echo handler.

Once the server is started, the pipeline will receive requests, process them via the auth middleware, and then either stop there (if a response was sent by the auth handler) or continue to the _echo handler if the auth handler validated a request and sent it further down the pipeline.

Configuration

Next up is the first utility class, in lib/config.dart:

The Config class makes use of the dotenv package internally, to load an environment map from a file and then expose the properties port and secret to the rest of the program. This approach makes it easy to upgrade the dotenv package or even replace it with another working solution entirely, with no effect on the rest of the system. It also allows for setting up failsafe logic to catch when variables are missing or have incorrect values.

Generating Hashes

The second utility class, for generating hashes, is in lib/hash.dart:

This simple Hash class serves one purpose: to generate SHA256 hashes to be used instead of plaintext passwords. While this is just an example app, it’s generally best to avoid the use of plaintext passwords at all cost, even when just prototyping something. Take five minutes and use hashes.

The Auth Provider

The next file we’ll take a look at is lib/auth-provider.dart:

The AuthProvider class handles the actual authentication for this API. An instance of JsonDecoder is created to handle deserialization of the login request body from JSON. The _check method is a utility that returns true if a user data Map object matches one passed in from the list of users hard-coded into the class for example purposes. In a real app, this list of users would be stored in some database and/or managed by a service, but for test purposes, it’s handy to mock something up fast that works 100% of the time.

The handle method is called by the middleware handler during request processing, and will either return a Response if the request can be handled internally (such as handling a login or rejecting a bad token for an API call), or it will return null if no action is required, allowing the request to continue to the next middleware or handler in the pipe (in our case, echoing the request).

To implement this, the handler checks the request url string and will invoke the auth method if the url is 'login' or the verify method otherwise.

The auth method first deserializes the request JSON and extracts the required properties from it, storing the username and a hashed variant of the password inside creds, and then searching the hard-coded list of users for an entity that returns a match when tested with the _check method. If one exists, a token is created with the username along with issuer and audience claims. For more information about JWT claims and implementation, refer to this page.

The verify method works in reverse, retrieving a JWT from the Authorization header (after removing the 'Bearer: ' component) and either validating the request and allowing it to continue down the pipeline, or rejecting it outright if either the token itself was invalid or if it’s claims didn’t match the ones hard-coded into this app (‘Acme Widgets Corp’ and ‘example.com’).

Testing

To test out the API, start the server with $ dart bin/server.dart from within the project directory. The message Serving at http://localhost:3000 should be returned, indicating that it’s ready to test. First, let’s check that our non-authenticated requests are being rejected:

$ curl localhost:3000/hello

This should return the response Authorization rejected. Next, test the login with a username/password from the hardcoded list:

$ curl localhost:3000/login -d \ '{"username":"test","password":"insecure"}'

If all goes well, this will return a JSON web token (screenshots below). Copy this string and paste it in place of MY_JWT below:

$ curl localhost:3000/hello -H "Authorization: Bearer MY_JWT"

This should return Authorization OK for “hello” indicating that our previously-denied request is now being granted, which means that the API is working as expected and requests are being handled properly.

Conclusion

This article demonstrates some of the features that make Dart a great choice for server-side development, such as a clean and expressive syntax and a well-maintained and easy-to-use package system. This example project (available here) makes a good starting point for building more complex API solutions to support desktop, mobile, and web clients of any type.

Thanks for reading and good luck with your next DevOps project!

Screen capture of a JWT auth test session

Kenneth Reilly (8_bit_hacker) is CTO of LevelUP

--

--