🔐 Authentication
Overview
Section titled “Overview”In this tutorial, we’re going to build an app that exposes endpoints accessible only to users that have been authenticated.
When we’re done, we should be able to authenticate with a user credentials, and access the protected routes.
Like mentioned in the Dart Frog Authentication documentation, there are many ways of implementing authentication in a backend, for this tutorial we will use a hardcoded, in memory, database of users and JSON Web Tokens (or for short JWTs) for the user’s authentication token.
Creating a new app
Section titled “Creating a new app”To create a new Dart Frog app, open your terminal, change to the directory where you’d like to create the app, and run the following command:
dart_frog create authenticated_appYou should see an output similar to:
✓ Creating authenticated_app (0.1s)✓ Installing dependencies (1.7s)
Created authenticated_app at ./authenticated_app.
Get started by typing:
cd ./authenticated_appdart_frog devRunning the development server
Section titled “Running the development server”You should now have a directory called authenticated_app. Let’s change
directories into the newly created project:
cd authenticated_appThen, run the following command:
dart_frog devThis will start the development server on port 8080:
✓ Running on http://localhost:8080 (1.3s)The Dart VM service is listening on http://127.0.0.1:8181/YKEF_nbwOpM=/The Dart DevTools debugger and profiler is available at: http://127.0.0.1:8181/YKEF_nbwOpM=/devtools/#/?uri=ws%3A%2F%2F127.0.0.1%3A8181%2FYKEF_nbwOpM%3D%2Fws[hotreload] Hot reload is enabled.Make sure it’s working by opening http://localhost:8080
in your browser or via cURL:
curl --request GET \ --url http://localhost:8080If everything succeeded, you should see Welcome to Dart Frog!.
The hardcode domain code
Section titled “The hardcode domain code”To keep the tutorial simple and focused on authentication, our database of users
will be hardcoded and the User model will be simple, containing just an id and
a name.
For next steps, use the code below to create the domain classes.
class User { const User({ required this.id, required this.name, required this.password, });
final String id; final String name; final String password;}import 'package:authenticated_app/user.dart';
class Authenticator { static const _users = { 'john': User( id: '1', name: 'John', password: '123', ), 'jack': User( id: '2', name: 'Jack', password: '321', ), };
static const _passwords = { // ⚠️ Never store user's password in plain text, these values are in plain text // just for the sake of the tutorial. 'john': '123', 'jack': '321', };
User? findByUsernameAndPassword({ required String username, required String password, }) { final user = _users[username];
if (user != null && _passwords[username] == password) { return user; }
return null; }}We also need to provide our Authenticator to our routes. It will be used by
the sign in route and all others that require authentication.
In order to do so, we can use
Dart frog’s dependency injection and create a
middleware in the root of our routes folder with the following code:
import 'package:authenticated_app/authenticator.dart';import 'package:dart_frog/dart_frog.dart';
Handler middleware(Handler handler) { return handler.use( provider<Authenticator>( (_) => Authenticator(), ), );}Writing a Sign In route
Section titled “Writing a Sign In route”Now that we have all the domain code necessary to authenticate users given an username and a password, we can create a route to authenticate users. Within the routes folder, create the file below:
import 'dart:io';
import 'package:authenticated_app/authenticator.dart';import 'package:dart_frog/dart_frog.dart';
Future<Response> onRequest(RequestContext context) async { return switch (context.request.method) { HttpMethod.post => _onPost(context), _ => Future.value( Response(statusCode: HttpStatus.methodNotAllowed), ), };}
Future<Response> _onPost(RequestContext context) async { final body = await context.request.json() as Map<String, dynamic>; final username = body['username'] as String?; final password = body['password'] as String?;
if (username == null || password == null) { return Response(statusCode: HttpStatus.badRequest); }
final authenticator = context.read<Authenticator>();
final user = authenticator.findByUsernameAndPassword( username: username, password: password, );
if (user == null) { return Response(statusCode: HttpStatus.unauthorized); } else { return Response.json( body: { 'token': username }, ); }}To people familiar with Dart Frog, the code above should be no real challenge, we are simply handling the request in the following steps:
- Check if we have all the info needed, returning
badRequestotherwise. - Get our
Authenticatordependency from our dependency injection. - Use the
Authenticatorto get a user that match the request’s credential. - Return an
unauthorized(401) if there is no user, or return the user username as the authentication token otherwise.
Using the username as an authentication token is quite unsafe. Malicious hackers could easily modify messages with usernames to break through our security and impersonate someone else. For the sake of simplicity, we’ll finish off using this simple authentication setup before introducing JWT, a more complex security method.
Try now running a curl in the terminal to get a token:
curl -X POST \ -H "Content-Type: application/json" \ -d '{"username": "john", "password": "123"}' \ http://localhost:8080/sign_in
# {"token":"john"}Requiring authentication to access routes
Section titled “Requiring authentication to access routes”Now that we have the means to get an authentication token, we can now protect routes by requiring a token to access them.
To start, let’s create the following route:
import 'dart:io';
import 'package:dart_frog/dart_frog.dart';
Future<Response> onRequest(RequestContext context) { return switch (context.request.method) { HttpMethod.post => _onPost(context), _ => Future.value(Response(statusCode: HttpStatus.methodNotAllowed)), };}
Future<Response> _onPost(RequestContext context) async { final task = await context.request.body(); return Response.json( body: { 'recorded_task': task, }, );}This route doesn’t do much for now, it just reads the request and answers with a body that includes the received task name. In addition, it is yet to be protected.
To protect our route, we will use
dart_frog_auth, a package provided
by Dart Frog that makes it easier to implement token based authentications.
So let’s start by adding it to the project:
dart pub add dart_frog_authFirst, we need to create a method in our Authenticator class which will be
responsible of validating an authentication token. Since for now, our
authentication token is just the username, we can add the following snippet to
the Authenticator class:
User? verifyToken(String username) { return _users[username]; }If the token is valid, the user will be returned, otherwise, the method will
return null.
Next, lets create the following middleware under routes/tasks:
import 'package:authenticated_app/authenticator.dart';import 'package:authenticated_app/user.dart';import 'package:dart_frog/dart_frog.dart';import 'package:dart_frog_auth/dart_frog_auth.dart';
Handler middleware(Handler handler) { return handler.use( bearerAuthentication<User>( authenticator: (context, token) async { final authenticator = context.read<Authenticator>(); return authenticator.verifyToken(token); }, ), );}This middleware is adding a Bearer Token authentication for all requests
incoming within the tasks namespace.
The usage of the bearerAuthentication middleware, which is provided by
dart_frog_auth, is quite simple. We need to provide a function that returns a
user if the token is valid and belongs to a user; or null otherwise.
This middleware will automatically return an unauthorized (401) response to
incoming requests when no valid tokens are provided. So, if we go ahead and try
the following command in our terminal:
# Note the additional `-v`, so we can see the status code in the outputcurl -d "Buy bread" \ -v \ http://localhost:8080/tasksWe should see the following line in the output:
< HTTP/1.1 401 UnauthorizedBut if we inform a valid and correct authentication token:
curl -d "Buy bread" \ -v \ -H "Authorization: Bearer john" \ http://localhost:8080/tasksThen the correct response of that route should be the output:
{"recorded_task":"Buy bread"}Additionally, the bearerAuthentication middleware will provide the returned
user in the request context, so any route handlers affected by it, will already
have access to the user that is currently authenticated.
With that information, we can change our tasks routes to have a more interesting response:
import 'dart:io';
import 'package:authenticated_app/user.dart';import 'package:dart_frog/dart_frog.dart';
Future<Response> onRequest(RequestContext context) { return switch (context.request.method) { HttpMethod.post => _onPost(context), _ => Future.value(Response(statusCode: HttpStatus.methodNotAllowed)), };}
Future<Response> _onPost(RequestContext context) async { final task = await context.request.body(); final user = context.read<User>();
return Response.json( body: { 'recorded_task': task, 'user_id': user.id, }, );}Protecting the authentication token
Section titled “Protecting the authentication token”This is a good point to review what we have done so far:
- We have created a sign in route, where credentials can be posted, and an authentication token is returned if valid.
- We have routes that can only be accessed if an authentication token is sent in the request.
However, as we noticed in the steps before, our authentication token is quite unsafe, it is nothing more than the username. Meaning that if a malicious hacker guesses another username, which is not something hard to do. This would allow the hacker to impersonate the user and perform bad actions, such as stealing information.
To avoid that, we have to make our tokens in a way where they cannot be faked, guessed or tampered. There are many methods of doing so, in this tutorial, we will use JWT, a widely used standard in the industry to secure issued tokens. This tutorial will not go much in deep on how JWT tokens work under the hood, so to get a better understanding on how they work, be sure to check their official documentation.
Right, given this brief introduction, let’s get to it! Luckily, the Dart ecosystem already has a handy package that makes it easy to work with JWT tokens. We can start by adding that dependency to our project:
dart pub add dart_jsonwebtokenNext, add the following method to our Authenticator class:
String generateToken({ required String username, required User user, }) { final jwt = JWT( { 'id': user.id, 'name': user.name, 'username': username, }, );
return jwt.sign(SecretKey('123')); }This new method takes care of generating a JWT token. Note that we sign out
the token by passing a secret key, as the name implies, this key is secret and
should be kept as such. For the scope of this tutorial we are keeping it hard
coded. Nonetheless, in a real case application, be sure to correctly store it
and pass it to the code in a way where then will remain secret to outsiders.
The sign method will create a signature out of the data we passed to it, and
it will be part of the token. This technique will allow us to check if an
authentication token that we’ve received is valid and if it hasn’t been
tampered!
Alright, now we need to update our /sign_in route. It should no longer return
the user’s username, but instead, the token created by the generateToken
method. The route should now look like this:
import 'dart:io';
import 'package:authenticated_app/authenticator.dart';import 'package:dart_frog/dart_frog.dart';
Future<Response> onRequest(RequestContext context) async { return switch (context.request.method) { HttpMethod.post => _onPost(context), _ => Future.value( Response(statusCode: HttpStatus.methodNotAllowed), ), };}
Future<Response> _onPost(RequestContext context) async { final body = await context.request.json() as Map<String, dynamic>; final username = body['username'] as String?; final password = body['password'] as String?;
if (username == null || password == null) { return Response(statusCode: HttpStatus.badRequest); }
final authenticator = context.read<Authenticator>();
final user = authenticator.findByUsernameAndPassword( username: username, password: password, );
if (user == null) { return Response(statusCode: HttpStatus.unauthorized); } else { return Response.json( body: { 'token': authenticator.generateToken( username: username, user: user, ), }, ); }}Finally, we now need to change the Authenticator to verify the newly signed
token, instead of just checking against a user’s username.
User? verifyToken(String token) { try { final payload = JWT.verify( token, SecretKey('123'), );
final payloadData = payload.payload as Map<String, dynamic>;
final username = payloadData['username'] as String; return _users[username]; } catch (e) { return null; } }And that is it, with the addition of a signed token, if someone tampers the information stored in it, or tries to forge a token without knowing the secret key, the authentication will fail; only real authenticated users will be able to access protected routes!
🎉 Congratulations, you’ve created an application using Dart Frog with authentication.