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:
- Route matched →
auth()called → if returnsfalse, request stopscheckPermission()called → if returnsfalse, request stops- 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.