search ESC

Searching…

No results for "".

Type at least 2 characters to search.

Docs
You are viewing an older version (v1.0.0-alpha.1). Go to the latest.

Authentication

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

// 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:

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:

'providers': [
  (app) => AuthServiceProvider(app),
  // ...
],

The Auth Facade

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?

// 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:

abstract class Guard {
  Future login(Map data, Authenticatable user);
  Future logout();
  bool check();
  bool get guest;
  T? user();
  dynamic id();
  Future hasToken();
  Future getToken();
  Future refreshToken();
  Future restore();
}

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:

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

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();
  }
}

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.

// 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:

// 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 handle(void Function() next) async {
    if (Auth.check()) {
      MagicRoute.to('/dashboard');
    } else {
      next();
    }
  }
}

Login & Logout

Login Flow

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

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:

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.