# Authentication
- [Introduction](#introduction)
- [Quick Start](#quick-start)
- [Configuration](#configuration)
- [The Auth Facade](#the-auth-facade)
- [Guards](#guards)
- [Built-in Guards](#built-in-guards)
- [Custom Guards](#custom-guards)
- [Reactive Auth State](#reactive-auth-state)
- [Auto Token Refresh](#auto-token-refresh)
- [Protecting Routes](#protecting-routes)
- [Login & Logout](#login--logout)
## Introduction
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.
### Key Features
| 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) |
## Quick Start
```dart
// 1. Register user factory (tells Magic how to create User from API data)
Auth.registerModel(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();
print('Welcome, ${user?.name}');
}
// 4. Logout
await Auth.logout();
MagicRoute.to('/login');
```
## Configuration
Create `lib/config/auth.dart`:
```dart
Map 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`:
```dart
'providers': [
(app) => AuthServiceProvider(app),
// ...
],
```
## The Auth Facade
The `Auth` facade provides convenient access to authentication functionality:
```dart
// 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?
// 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
Guards define how users are authenticated. Each guard implements the `Guard` contract:
```dart
abstract class Guard {
Future login(Map data, Authenticatable user);
Future logout();
bool check();
bool get guest;
T? user();
dynamic id();
void setUser(Authenticatable user);
Future hasToken();
Future getToken();
Future refreshToken();
Future restore();
/// Bumped on every auth state change (login, logout, restore).
ValueNotifier get stateNotifier;
}
```
### Built-in Guards
| Guard | Login Data | Use Case |
|-------|-----------|----------|
| `BearerTokenGuard` | `token`, `refresh_token` | JWT/OAuth APIs |
| `BasicAuthGuard` | `username`, `password` | Basic HTTP Auth |
| `ApiKeyGuard` | `api_key` | API Key authentication |
### Custom Guards
Create custom guards by extending `BaseGuard`:
```dart
class MyGuard extends BaseGuard {
MyGuard() : super(
userEndpoint: '/api/me',
refreshEndpoint: '/api/refresh',
userFactory: (data) => User.fromMap(data),
);
@override
Future login(Map 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());
```
### Firebase Guard Example
```dart
class FirebaseGuard extends BaseGuard {
final _auth = firebase.FirebaseAuth.instance;
FirebaseGuard() : super(userFactory: (data) => User.fromMap(data));
@override
Future login(Map data, Authenticatable user) async {
final idToken = await _auth.currentUser?.getIdToken();
if (idToken != null) await storeToken(idToken);
await cacheUser(user);
setUser(user);
}
@override
Future 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 logout() async {
await _auth.signOut();
await super.logout();
}
}
```
## Reactive Auth State
Every guard exposes a `ValueNotifier stateNotifier` that increments on every auth state change — `setUser()`, `logout()`, and session restore. Use it to reactively rebuild UI without manual state management.
```dart
// Using ListenableBuilder (Flutter built-in)
ListenableBuilder(
listenable: Auth.guard.stateNotifier,
builder: (context, _) {
if (Auth.check()) {
return Text('Hello, ${Auth.user()?.name}');
}
return const Text('Not logged in');
},
)
```
With `MagicBuilder` the pattern is identical — pass `stateNotifier` as the listenable:
```dart
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.
## Auto Token Refresh
When `auto_refresh` is enabled, Magic automatically handles 401 responses:
1. Original request fails with 401
2. Interceptor calls `Auth.refreshToken()`
3. If refresh succeeds, original request is retried with new token
4. If refresh fails, user is logged out
The auth interceptor is built into `AuthServiceProvider` and works automatically when configured.
```dart
// Manual token refresh
final success = await Auth.refreshToken();
if (!success) {
await Auth.logout();
MagicRoute.to('/login');
}
```
## Protecting Routes
Use the `auth` middleware to protect routes:
```dart
// 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:
```dart
class RedirectIfAuthenticated extends MagicMiddleware {
@override
Future handle(void Function() next) async {
if (Auth.check()) {
MagicRoute.to('/dashboard');
} else {
next();
}
}
}
```
## Login & Logout
### Login Flow
```dart
class AuthController extends MagicController with ValidatesRequests {
Future login(Map 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');
}
}
}
```
### Logout Flow
```dart
Future 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');
}
```
### Restoring Session on App Start
In your `main.dart`:
```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.