Auth Controller

AuthController is an abstract class that extends Controller. It provides the authentication and authorization contract for protecting routes. You implement it once per application and attach it to any route that requires login.

Abstract Methods

Method When it runs What to return
auth() On every request to a protected route true to allow, false to reject (and handle the response)
authApi() On requests to protected /api/ endpoints true to allow
checkLogin() Called by auth() to validate the session A record ({bool success, String message, T? user})
checkPermission() After auth() passes, checks route.permissions true if user has the required permission
loginPost() Handles POST to the login form route HTML response
register() Handles registration form submission, if your app supports self-registration HTML response
newUser() Creates a new user account (e.g. from an admin panel) HTML response
logout() Handles the logout route Redirect or HTML
updateAuth(email, password, user) After successful login Store session/cookie
removeAuth() On logout Clear session/cookie
index() Inherited from Controller — the default handler if the route has no index/controller action of its own Whatever this route should show

If your app doesn't need registration, it's common to leave register()/newUser() throwing UnimplementedError(), as the example project does — see the example below.

Complete Example

This is the auth controller from the Finch example project:

import 'package:finch/finch_route.dart';
import 'package:finch/finch_tools.dart';
import '../models/mock_user_model.dart';
import '../route/web_route.dart';

class AppAuthController extends AuthController<MockUserModel> {
  MockUserModel? userLogined;

  @override
  Future<bool> auth() async {
    var res = await checkLogin();
    if (!res.success) {
      // API requests get a JSON 403, web requests get a redirect
      if (rq.isApiEndpoint) {
        await rq.renderError(
          403,
          toData: true,
          params: {'message': 'Please login.', 'success': false},
        );
      } else {
        await rq.redirect('/example/form');
      }
      return false;
    }
    updateAuth(res.user!.email, res.user!.password, res.user!);
    return true;
  }

  @override
  Future<bool> authApi() async {
    var auth = rq.authorization;
    var mockUser = MockUserModel();

    if (auth.type == AuthType.basic) {
      String email = auth.getBasicUsername();
      String password = auth.getBasicPassword();
      return email == mockUser.email && password == mockUser.password;
    } else if (auth.type == AuthType.bearer) {
      return auth.value == '${mockUser.email} ${mockUser.password}';
    }
    return false;
  }

  @override
  Future<({bool success, String message, MockUserModel? user})>
      checkLogin() async {
    var mockUser = MockUserModel();
    var userSession = rq.getSession('user', def: '');

    if (userSession == mockUser.email) {
      return (success: true, message: 'Success.', user: mockUser);
    }
    return (success: false, message: 'Please login.', user: mockUser);
  }

  @override
  Future<bool> checkPermission() async {
    if (rq.route == null || userLogined == null) return false;

    var permission = userLogined!.permission;
    if (rq.route!.permissions.isNotEmpty &&
        !rq.route!.permissions.contains(permission)) {
      return false;
    }
    return true;
  }

  @override
  Future<String> loginPost() async {
    var formLogin = LoginForm();
    await formLogin.check(
      onInvalid: (p0) {},
      onValid: (p0) {
        var mockUser = MockUserModel();
        var email = formLogin.get<String>('email', def: '');
        var password = formLogin.get<String>('password', def: '');
        if (email == mockUser.email && password == mockUser.password) {
          updateAuth(email, password, mockUser);
        } else {
          rq.addParam('errorLogin', 'form.validation.loginError'.tr);
        }
      },
    );
    return homeController.renderView('example/form');
  }

  @override
  Future<String> logout() {
    removeAuth();
    return rq.redirect('/example/form');
  }

  @override
  Future<String> newUser() => throw UnimplementedError();

  @override
  Future<String> register() => throw UnimplementedError();

  @override
  void removeAuth() {
    rq.session.remove('user');
    rq.removeCookie('user');
    userLogined = null;
  }

  @override
  void updateAuth(String email, String password, MockUserModel user) {
    userLogined = user;
    rq.addSession('user', email);
  }
}

Attaching to a Route

final authController = AppAuthController();

FinchRoute(
  key: 'example.panel',
  path: 'panel',
  methods: Methods.ALL,
  auth: authController,
  permissions: ['admin'],
  index: homeController.exampleAuth,
),

Request flow for a protected route:

  1. Route matched →
  2. auth() called → if returns false, request stops
  3. checkPermission() called → if returns false, request stops
  4. Controller / index handler executes

Authorization Header

For API routes, read the Authorization header via rq.authorization:

var auth = rq.authorization;

if (auth.type == AuthType.bearer) {
  String token = auth.value;
  // validate token
}

if (auth.type == AuthType.basic) {
  String user = auth.getBasicUsername();
  String pass = auth.getBasicPassword();
  // validate credentials
}

AuthType also declares digest, hawk, aws, and akamai values for forward compatibility, but only basic and bearer are actually parsed today — any other scheme (or a missing/malformed header) resolves to AuthType.none.

Permissions

Permissions is a small class of predefined permission-string constants you can use instead of hardcoding strings everywhere:

class Permissions {
  static final String none = 'none';
  static final String superAdmin = 'super-admin';
}
FinchRoute(
  key: 'admin.panel',
  path: 'admin/panel',
  auth: authController,
  permissions: [Permissions.superAdmin],
  index: adminController.panel,
),

It only defines these two constants — for anything beyond none/super-admin, define your own permission strings (or extend Permissions) and check them the same way inside checkPermission(), as the example project's AppAuthController.checkPermission() does above.