I'm using the shelf package from Dart to implement a backend for the realworld-example-app. According to the specs, some routes require authentication, some don't, and in others the authentication is optional. My idea is to write a middleware that would decode and validate the token and put the User
in the request context if it was found, or else return 401 status in case of errors.
However, as I'm not very experienced with Dart
, shelf
, or Dart
patterns, the approach I ended up implementing was awkward: I hardcoded some routes that would require authentication as a static
property of the AppRouter
class below, require a List
of routes as a parameter to the authorize
middleware function, and then call it in the createServer
function. This approach is leads to code duplication, non-obvious coupling and is ugly. Do you have better ways of doing this?
import 'package:dart_shelf_realworld_example_app/src/users/jwt_service.dart';
import 'package:dart_shelf_realworld_example_app/src/users/users_handlers.dart';
import 'package:dart_shelf_realworld_example_app/src/users/users_service.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
class AppRouter {
// Routes without the starting slash
static List<String> routesRequiringAuthorization = ['api/user'];
late UsersHandlers _usersHandlers;
AppRouter(
{required JwtService jwtService, required UsersService usersService}) {
_usersHandlers =
UsersHandlers(jwtService: jwtService, usersService: usersService);
}
Handler get router {
final router = Router();
router.post('/api/users', _usersHandlers.registerUserHandler);
router.post('/api/users/login', _usersHandlers.loginUserHandler);
router.get('/api/user', _usersHandlers.getCurrentUserHandler);
router.put('/api/user', _usersHandlers.updateUserHandler);
return router;
}
}
import 'package:dart_shelf_realworld_example_app/src/users/jwt_service.dart';
import 'package:dart_shelf_realworld_example_app/src/users/users_service.dart';
import 'package:shelf/shelf.dart';
Middleware authorize(UsersService usersService, JwtService jwtService,
List<String> routesRequiringAuthorization) =>
(innerHandler) {
return (request) async {
var isAuthorizationRequired =
routesRequiringAuthorization.contains(request.url.path);
if (isAuthorizationRequired) {
final authorizationHeader = request.headers['Authorization'] ??
request.headers['authorization'];
if (authorizationHeader == null) {
return Response(401);
}
if (!authorizationHeader.startsWith('Token ')) {
return Response(401);
}
final token = authorizationHeader.replaceFirst('Token', '').trim();
if (token.isEmpty) {
return Response(401);
}
final userTokenClaim = jwtService.getUserTokenClaim(token);
if (userTokenClaim == null) {
return Response(401);
}
final user = await usersService.getUserByEmail(userTokenClaim.email);
if (user == null) {
return Response(401);
}
request = request.change(context: {'user': user});
}
return Future.sync(() => innerHandler(request)).then((response) {
return response;
});
};
};
import 'dart:io';
import 'package:dart_shelf_realworld_example_app/src/app_router.dart';
import 'package:dart_shelf_realworld_example_app/src/common/middleware/authorize.dart';
import 'package:dart_shelf_realworld_example_app/src/users/jwt_service.dart';
import 'package:dart_shelf_realworld_example_app/src/users/users_service.dart';
import 'package:postgres/postgres.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart';
Future<HttpServer> createServer() async {
final authSecretKey = Platform.environment['AUTH_SECRET_KEY'];
final authIssuer = Platform.environment['AUTH_ISSUER'];
final dbHost = Platform.environment['DB_HOST'];
final envDbPort = Platform.environment['DB_PORT'];
final dbName = Platform.environment['DB_NAME'];
final dbUser = Platform.environment['DB_USER'];
final dbPassword = Platform.environment['DB_PASSWORD'];
if (authSecretKey == null) {
throw StateError('Environment variable AUTH_SECRET_KEY is required');
}
if (authIssuer == null) {
throw StateError('Environment variable AUTH_ISSUER is required');
}
if (dbHost == null) {
throw ArgumentError('Environment variable DB_HOST must be an integer');
}
if (envDbPort == null) {
throw StateError('Environment variable DB_PORT is required');
}
if (dbName == null) {
throw StateError('Environment variable DB_NAME is required');
}
final dbPort = int.tryParse(envDbPort);
if (dbPort == null) {
throw ArgumentError('Environment variable DB_PORT must be an integer');
}
final connection = PostgreSQLConnection(dbHost, dbPort, dbName,
username: dbUser, password: dbPassword);
await connection.open();
final usersService = UsersService(connection: connection);
final jwtService = JwtService(issuer: authIssuer, secretKey: authSecretKey);
final router =
AppRouter(jwtService: jwtService, usersService: usersService).router;
// Use any available host or container IP (usually `0.0.0.0`).
final ip = InternetAddress.anyIPv4;
// Configure a pipeline that logs requests.
final handler = Pipeline()
.addMiddleware(logRequests())
.addMiddleware(authorize(
usersService, jwtService, AppRouter.routesRequiringAuthorization))
.addHandler(router);
final port = int.parse(Platform.environment['PORT'] ?? '8080');
// For running in containers, we respect the PORT environment variable.
final server = await serve(handler, ip, port);
print('Server listening on port ${server.port}');
return server;
}