- Introduction
- Quick Start
- Configuration
- The Auth Facade
- Guards
- Reactive Auth State
- Auto Token Refresh
- Protecting Routes
- Login & Logout
Magic provides a simple, frontend-focused authentication system with user caching and automatic token refresh. Like Laravel's Auth system, it's built around the concept of "guards" that define how users are authenticated.
| Feature | Description |
|---|---|
| User Caching | Instant restore from secure cache, then sync from API |
| Auto Token Refresh | 401 response → refresh token → retry original request |
| Multiple Guards | Support for Bearer, Basic, API Key, or custom guards |
| Secure Storage | Tokens stored in platform secure storage (Keychain/Keystore) |
// 1. Register user factory (tells Magic how to create User from API data)
Auth.registerModel<User>(User.fromMap);
// 2. Login after API call
final response = await Http.post('/login', data: {
'email': email,
'password': password,
});
if (response.successful) {
final user = User.fromMap(response['data']['user']);
await Auth.login({
'token': response['data']['token'],
'refresh_token': response['data']['refresh_token'],
}, user);
MagicRoute.to('/dashboard');
}
// 3. Check authentication anywhere
if (Auth.check()) {
final user = Auth.user<User>();
print('Welcome, ${user?.name}');
}
// 4. Logout
await Auth.logout();
MagicRoute.to('/login');Create lib/config/auth.dart:
Map<String, dynamic> get authConfig => {
'auth': {
'defaults': {
'guard': 'api',
},
'guards': {
'api': {
'driver': 'bearer',
},
},
'endpoints': {
'user': '/api/user',
'refresh': '/api/auth/refresh',
},
'token': {
'key': 'auth_token',
'header': 'Authorization',
'prefix': 'Bearer',
},
'auto_refresh': true,
},
};Register in your config and add AuthServiceProvider:
'providers': [
(app) => AuthServiceProvider(app),
// ...
],The Auth facade provides convenient access to authentication functionality:
// Check if user is authenticated
Auth.check() // bool
// Check if user is a guest (not authenticated)
Auth.guest() // bool
// Get the authenticated user
Auth.user<User>() // User?
// Get user ID
Auth.id() // dynamic
// Login
await Auth.login(tokenData, user)
// Logout
await Auth.logout()
// Restore session from cache
await Auth.restore()
// Manually refresh token
await Auth.refreshToken()
// Token management
await Auth.hasToken() // bool
await Auth.getToken() // String?Guards define how users are authenticated. Each guard implements the Guard contract:
abstract class Guard {
Future<void> login(Map<String, dynamic> data, Authenticatable user);
Future<void> logout();
bool check();
bool get guest;
T? user<T>();
dynamic id();
void setUser(Authenticatable user);
Future<bool> hasToken();
Future<String?> getToken();
Future<bool> refreshToken();
Future<void> restore();
/// Bumped on every auth state change (login, logout, restore).
ValueNotifier<int> get stateNotifier;
}| Guard | Login Data | Use Case |
|---|---|---|
BearerTokenGuard |
token, refresh_token |
JWT/OAuth APIs |
BasicAuthGuard |
username, password |
Basic HTTP Auth |
ApiKeyGuard |
api_key |
API Key authentication |
Create custom guards by extending BaseGuard:
class MyGuard extends BaseGuard {
MyGuard() : super(
userEndpoint: '/api/me',
refreshEndpoint: '/api/refresh',
userFactory: (data) => User.fromMap(data),
);
@override
Future<void> login(Map<String, dynamic> data, Authenticatable user) async {
await storeToken(data['token'], data['refresh_token']);
await cacheUser(user);
setUser(user);
}
}
// Register in your auth config
Auth.manager.extend('myguard', (config) => MyGuard());class FirebaseGuard extends BaseGuard {
final _auth = firebase.FirebaseAuth.instance;
FirebaseGuard() : super(userFactory: (data) => User.fromMap(data));
@override
Future<void> login(Map<String, dynamic> data, Authenticatable user) async {
final idToken = await _auth.currentUser?.getIdToken();
if (idToken != null) await storeToken(idToken);
await cacheUser(user);
setUser(user);
}
@override
Future<void> restore() async {
// Try cached user first (instant UI)
final cached = await loadCachedUser();
if (cached != null) setUser(cached);
// Then verify with Firebase
final fbUser = _auth.currentUser;
if (fbUser == null) {
await logout();
return;
}
final token = await fbUser.getIdToken();
if (token != null) await storeToken(token);
final user = userFactory!({
'id': fbUser.uid,
'email': fbUser.email,
'name': fbUser.displayName,
});
setUser(user);
await cacheUser(user);
}
@override
Future<void> logout() async {
await _auth.signOut();
await super.logout();
}
}Every guard exposes a ValueNotifier<int> stateNotifier that increments on every auth state change — setUser(), logout(), and session restore. Use it to reactively rebuild UI without manual state management.
// Using ListenableBuilder (Flutter built-in)
ListenableBuilder(
listenable: Auth.guard.stateNotifier,
builder: (context, _) {
if (Auth.check()) {
return Text('Hello, ${Auth.user<User>()?.name}');
}
return const Text('Not logged in');
},
)With MagicBuilder the pattern is identical — pass stateNotifier as the listenable:
MagicBuilder(
listenable: Auth.guard.stateNotifier,
builder: (context, _) => Auth.check()
? const DashboardView()
: const LoginView(),
)setUser() is called internally by login(), restore(), and any custom guard that calls setUser(user) directly. Each call bumps stateNotifier.value by 1, triggering all registered listeners.
When auto_refresh is enabled, Magic automatically handles 401 responses:
- Original request fails with 401
- Interceptor calls
Auth.refreshToken() - If refresh succeeds, original request is retried with new token
- If refresh fails, user is logged out
The auth interceptor is built into AuthServiceProvider and works automatically when configured.
// Manual token refresh
final success = await Auth.refreshToken();
if (!success) {
await Auth.logout();
MagicRoute.to('/login');
}Use the auth middleware to protect routes:
// Single route
MagicRoute.page('/dashboard', () => DashboardView())
.middleware(['auth']);
// Route group
MagicRoute.group(
middleware: ['auth'],
routes: () {
MagicRoute.page('/dashboard', () => DashboardView());
MagicRoute.page('/profile', () => ProfileView());
MagicRoute.page('/settings', () => SettingsView());
},
);Create a guest middleware to redirect authenticated users:
class RedirectIfAuthenticated extends MagicMiddleware {
@override
Future<void> handle(void Function() next) async {
if (Auth.check()) {
MagicRoute.to('/dashboard');
} else {
next();
}
}
}class AuthController extends MagicController with ValidatesRequests {
Future<void> login(Map<String, dynamic> data) async {
clearErrors();
final response = await Http.post('/login', data: data);
if (response.successful) {
final user = User.fromMap(response['data']['user']);
await Auth.login({
'token': response['data']['token'],
'refresh_token': response['data']['refresh_token'],
}, user);
Magic.success('Success', 'Welcome back!');
MagicRoute.to('/dashboard');
} else {
handleApiError(response, fallback: 'Invalid credentials');
}
}
}Future<void> logout() async {
// Optionally notify backend
await Http.post('/logout');
// Clear local auth state
await Auth.logout();
Magic.info('Logged Out', 'See you next time!');
MagicRoute.to('/login');
}In your main.dart:
void main() async {
await Magic.init(...);
// Restore auth session from cache
await Auth.restore();
runApp(MagicApplication(...));
}This instantly restores the cached user for a fast startup, then syncs with the API in the background.
If userFactory is not set on the guard, the cache load and API sync steps are skipped gracefully — no error is thrown. Set userFactory via Auth.manager.setUserFactory() (or pass it to BaseGuard's constructor) during the boot phase to enable full session restore.