Auth Controller

AuthController is een abstracte klasse die Controller uitbreidt. Het biedt het authenticatie- en autorisatiecontract voor het beveiligen van routes. Je implementeert het één keer per applicatie en koppelt het aan elke route die inloggen vereist.

Abstracte methoden

Methode Wanneer wordt dit uitgevoerd Wat moet worden geretourneerd
auth() Bij elk verzoek naar een beveiligde route true om toe te staan, false om te weigeren (en het antwoord zelf af te handelen)
authApi() Bij verzoeken naar beveiligde /api/-endpoints true om toe te staan
checkLogin() Wordt aangeroepen door auth() om de sessie te valideren Een record ({bool success, String message, T? user})
checkPermission() Nadat auth() is geslaagd, controleert dit route.permissions true als de gebruiker de vereiste permissie heeft
loginPost() Handelt POST naar de inlogformulierroute af HTML-antwoord
register() Handelt de inzending van het registratieformulier af, als je app zelfregistratie ondersteunt HTML-antwoord
newUser() Maakt een nieuw gebruikersaccount aan (bijv. vanuit een adminpaneel) HTML-antwoord
logout() Handelt de uitlogroute af Doorverwijzing of HTML
updateAuth(email, password, user) Na succesvolle login Sessie/cookie opslaan
removeAuth() Bij uitloggen Sessie/cookie wissen
index() Overgeërfd van Controller — de standaardhandler als de route geen eigen index-/controller-actie heeft Wat deze route ook moet tonen

Als je app geen registratie nodig heeft, is het gebruikelijk om register()/newUser() een UnimplementedError() te laten werpen, zoals het voorbeeldproject doet — zie het voorbeeld hieronder.

Volledig voorbeeld

Dit is de auth-controller uit het Finch-voorbeeldproject:

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-verzoeken krijgen een JSON 403, webverzoeken krijgen een doorverwijzing
      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);
  }
}

Koppelen aan een route

final authController = AppAuthController();

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

Verzoekstroom voor een beveiligde route:

  1. Route komt overeen →
  2. auth() wordt aangeroepen → als dit false retourneert, stopt het verzoek
  3. checkPermission() wordt aangeroepen → als dit false retourneert, stopt het verzoek
  4. Controller-/index-handler wordt uitgevoerd

Authorization-header

Voor API-routes lees je de Authorization-header via rq.authorization:

var auth = rq.authorization;

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

if (auth.type == AuthType.basic) {
  String user = auth.getBasicUsername();
  String pass = auth.getBasicPassword();
  // valideer de inloggegevens
}

AuthType declareert ook de waarden digest, hawk, aws en akamai met het oog op toekomstige compatibiliteit, maar op dit moment worden alleen basic en bearer daadwerkelijk geparsed — elk ander schema (of een ontbrekende/onjuist gevormde header) resulteert in AuthType.none.

Permissies

Permissions is een kleine klasse met vooraf gedefinieerde permissiestring-constanten die je kunt gebruiken in plaats van overal strings hard te coderen:

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,
),

Het definieert alleen deze twee constanten — voor alles buiten none/super-admin definieer je je eigen permissiestrings (of breid je Permissions uit) en controleer je ze op dezelfde manier binnen checkPermission(), zoals de AppAuthController.checkPermission() van het voorbeeldproject hierboven doet.