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
logout() Handles the logout route Redirect or HTML
updateAuth(email, password, user) After successful login Store session/cookie
removeAuth() On logout Clear session/cookie

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
}