Skip to main content

Authentication 🔑

There are many different approaches, protocols, and services when tackling authentication in a backend, which can all be affected by the business logic of the application.

Because of this Dart Frog does not bundle any feature, helpers or resources for authentication out of the box. This means that developers have full freedom to implement server authentication in the best way that fits their needs.

Nevertheless, there are a few common patterns that can used in many different approaches to give the developer a head start. For example, there is a package called dart_frog_auth, which makes it easy for a simple authentication method to be implemented while also layering the foundation for more advanced authentication. See below for more details:

Dart Frog Auth

The authentication methods provided in dart_frog_auth are based on Authorization specification, as defined in General HTTP. Here you will find support for Basic and Bearer authentications, which are common authentication methods used by many developers.

Basic Authentication

Like its name infers, this is a basic authentication scheme that consists of the client sending a user's credentials in the Authorization header. The credentials should be concatenated by a colon and encoded in a base64 string. The encoded credentials are then set in the header as follows:

Authorization: Basic TOKEN

Due to the credentials being sent encoded and not encrypted, this authentication can be considered less secure, especially when used without HTTPS/TLS.

Bearer Authentication

Similar to the basic authentication scheme, the bearer authentication scheme sends a user's credentials to the header with a single token instead of a username and password.

The bearer token format is up to the issuing authority server to define. It commonly consists of an access token with encrypted information that the server can validate.

The header is defined as follows:

Authorization: Bearer TOKEN

Usage

Both authentication schemes described above can be applied in a Dart Frog server by adding middleware to the routes that needs to be secured.

Consider the following application:

lib/
|- user_repository.dart
routes/
|- admin/
| |- index.dart
|- posts/
|- index.dart

Routes under posts are public, so they don't require any kind of authentication, while on admin, only authenticated users can access their endpoints. It's worth noting that the user_repository.dart file under the lib folder offers methods to authenticate users.

Basic Method

To implement the basic authentication scheme on admin routes, a middleware file should be created under the admin folder with the following content:

// routes/admin/_middleware.dart
import 'package:dart_frog/dart_frog.dart';
import 'package:dart_frog_auth/dart_frog_auth.dart';
import 'package:blog/user.dart';

Handler middleware(Handler handler) {
final userRepository = ...;
return handler
.use(requestLogger())
.use(
basicAuthentication<User>(
authenticator: (context, username, password) {
final userRepository = context.read<UserRepository>();
return userRepository.fetchFromCredentials(username, password);
},
),
);
}

The authenticator parameter must be a method that receives three positional arguments (context, username and password) and returns a user if any is found for those credentials, otherwise it should return null.

If a user is returned (authenticated), it will be set in the request context and can be read by request handlers, for example:

import 'package:dart_frog/dart_frog.dart';
import 'package:blog/user.dart';

Response onRequest(RequestContext context) {
final user = context.read<User>();
return Response.json(body: {'user': user.id});
}

In the case of null being returned (unauthenticated), the middleware will automatically send an unauthorized 401 in the response.

Bearer Method

To implement the bearer authentication scheme on admin routes, the same logic used for the basic method can be applied:

// routes/admin/_middleware.dart
import 'package:dart_frog/dart_frog.dart';
import 'package:dart_frog_auth/dart_frog_auth.dart';
import 'package:blog/user.dart';

Handler middleware(Handler handler) {
final userRepository = ...;
return handler
.use(requestLogger())
.use(
bearerTokenAuthentication<User>(
authenticator: (context, token) {
final userRepository = context.read<UserRepository>();
return userRepository.fetchFromAccessToken(token);
}
),
);
}

The authenticator parameter must be a function that receives two positional argument the context and the token sent on the authorization header and returns a user if any is found for that token.

Again, just like in the basic method, if a user is returned, it will be set in the request context and can be read on request handlers, for example:

import 'package:dart_frog/dart_frog.dart';
import 'package:blog/user.dart';

Response onRequest(RequestContext context) {
final user = context.read<User>();
return Response.json(body: {'user': user.id});
}

In the case of null being returned (unauthenticated), the middleware will automatically send an unauthorized 401 in the response.

Filtering Routes

In many instances, developers will want to apply authentication to some routes, while not to others.

One of those can be described by looking at implementing a basic RESTful CRUD API. In order to make such an API that allows consumers to create, update, delete, and get user information, the following list of routes will need to be created:

  • POST /users: Creates a user
  • PATCH /users/[id]: Updates the user with the given id.
  • DELETE /users/[id]: Deletes the user with the given id.
  • GET /users/[id]: Returns the user with the given id.

Those endpoints can be translated to the following structure in a Dart Frog backend:

routes/
|- users/
|- index.dart // Handles the POST
|- [id].dart // Handles PATCH, DELETE and GET
|- _middleware.dart

It would make sense for the PATCH, DELETE, and GET routes to be authenticated ones, since only an authenticated user would be allowed to change this information.

To accomplish that, we need the middleware to apply authentication to all routes except POST.

Such behavior is possible with the use of the applies optional predicate:

Handler middleware(Handler handler) {
final userRepository = UserRepository();

return handler
.use(requestLogger())
.use(provider<UserRepository>((_) => userRepository))
.use(
basicAuthentication<User>(
authenticator: (context, username, password) {
final userRepository = context.read<UserRepository>();
return userRepository.userFromCredentials(username, password);
},
applies: (RequestContext context) async =>
context.request.method != HttpMethod.post,
),
);
}

In the above example, only routes that are not POST will have authentication checked.

Authentication vs. Authorization

Both Authentication and authorization are related, but are different concepts that are often confused.

Authentication is about WHO the user is, while authorization is about WHAT a user can do.

These concepts are related since we need to know who the user is in order to check if they can perform or not a given operation.

dart_frog_auth only solves the authentication part of the problem. To enforce authorization, it is up to the developer to implement it manually, or use an authorization issue system like OAuth2, for example.

In technical terms, a request should return 401 (Unauthorized) when authentication fails and 403 (Forbidden) when authorization failed.

The following snippet shows how authorization can be manually checked in DELETE /users/[id] route, where only the current logged user is allowed to delete itself:

Future<Response> onRequest(RequestContext context, String id) async {
return switch (context.request.method) {
HttpMethod.delete => _deleteUser(context, id),
_ => Future.value(Response(statusCode: HttpStatus.methodNotAllowed)),
};
}

Future<Response> _deleteUser(RequestContext context, String id) async {
// If there is no authenticated user, `dart_frog_auth` automatically
// responds with a 401.

final user = context.read<User>();
if (user.id != id) {
// If the current authenticated user, obtained via `context.read<User>` is
// not the same of the one of the incoming request, a forbidden is returned!
return Response(statusCode: HttpStatus.forbidden);
}
await context.read<UserRepository>().deleteUser(user.id);
return Response(statusCode: HttpStatus.noContent);
}