1
\$\begingroup\$

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?

https://github.com/marcusmonteirodesouza/dart-shelf-realworld-example-app/blob/f323b015e3b0a6abbe8ac49170c66de9c32fe931/lib/src/app_router.dart

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;
  }
}

https://github.com/marcusmonteirodesouza/dart-shelf-realworld-example-app/blob/f323b015e3b0a6abbe8ac49170c66de9c32fe931/lib/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: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;
        });
      };
    };

https://github.com/marcusmonteirodesouza/dart-shelf-realworld-example-app/blob/f323b015e3b0a6abbe8ac49170c66de9c32fe931/lib/app.dart

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;
}
\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

I figured out a better way: the handlers in shelf are composable, and by using the Pipeline class we can add middleware to compose handlers that will then be added to the router.

https://github.com/marcusmonteirodesouza/dart-shelf-realworld-example-app/blob/04dddb250592061986aa23d823cfee6ff37a5ab6/lib/src/users/users_router.dart#L155

Handler get router {
    final router = Router();

    router.post('/users', _registerUserHandler);

    router.post('/users/login', _loginUserHandler);

    router.get(
        '/user',
        Pipeline()
            .addMiddleware(authorize(usersService, jwtService))
            .addHandler(_getCurrentUserHandler));

    router.put(
        '/user',
        Pipeline()
            .addMiddleware(authorize(usersService, jwtService))
            .addHandler(_updateUserHandler));

    return router;
  }

This way, I could have fine grained control over which routes would require authentication and which would not by adding or not the middleware on them individually.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.