🔒 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
Section titled “Dart Frog Auth”The authentication methods provided in dart_frog_auth use different HTTP
headers depending on the method. Basic and Bearer authentication use the
Authorization header, as defined in
General HTTP,
while Cookie-based authentication uses the Cookie header, as defined in
HTTP Cookies. The
package provides support for Basic, Bearer, and Cookie-based authentications,
which are common authentication methods used by many developers.
Basic Authentication
Section titled “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 TOKENDue to the credentials being sent encoded and not encrypted, this authentication can be considered less secure, especially when used without HTTPS/TLS.
Bearer Authentication
Section titled “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 TOKENCookie-based Authentication
Section titled “Cookie-based Authentication”Cookie-based authentication stores user credentials in HTTP cookies that are
stored in a user’s web browser and automatically sent with each subsequent
request. This method uses the Cookie header to transmit authentication
information.
The cookies typically contain session tokens or encrypted user data that the server can validate to authenticate the user. Common implementations include session cookies, JWT tokens stored in cookies, or secure HTTP-only cookies.
The header is defined as follows:
Cookie: session_id=abc123; auth_token=xyz789Both 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.dartroutes/ |- admin/ | |- index.dart |- posts/ |- index.dartRoutes 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
Section titled “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:
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
Section titled “Bearer Method”To implement the bearer authentication scheme on admin routes, the same logic
used for the basic method can be applied:
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( bearerAuthentication<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.
Cookies Method
Section titled “Cookies Method”To implement cookie-based authentication, you can use the cookieAuthentication
middleware:
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( cookieAuthentication<User>( authenticator: (context, cookies) { final userRepository = context.read<UserRepository>(); return userRepository.fetchFromAccessCookies(cookies); } ), );}The authenticator parameter must be a function that receives two positional
argument the context and the cookies set in the cookie header and returns a user
if any is found for that token.
Just like in the basic and bearer methods, 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.
Public vs Private Routes
Section titled “Public vs Private 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 userPATCH /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.dartIt 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.
Optionally Authenticated Routes
Section titled “Optionally Authenticated Routes”In some cases, developers may want to allow a route to be accessed with or without authentication, and they would like to return a different response based on the authentication status.
To achieve this, we recommend the following approach:
// Create your user modelsealed class User {}
class AppUser extends User { final String id;}
class AnonymousUser extends User {}// Create your middlewareHandler middleware(Handler handler) { return handler.use(cookieAuthentication<User>( authenticator: (context, cookies) { final userRepository = context.read<UserRepository>(); final user = userRepository.getUserFromCookies(cookies); return user != null ? AppUser(user.id) : AnonymousUser(); }, ));}// Use it in your handlerResponse onRequest(RequestContext context) { final user = context.read<User>(); return user is AppUser ? AuthenticatedResponse() : UnauthenticatedResponse();}Authentication vs. Authorization
Section titled “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);}