Skip to content

App Flow | Authentication

App Flow offers a robust and flexible authentication layer designed to manage user sessions and enforce secure access across the application.

State management is integrated using Riverpod, enabling shared application states, such as routing, to be easily accessed within the authentication logic.

Authentication Structure

The authentication architecture is built around the AuthService interface, which defines the core methods required for user authentication. By default, App Flow supports two implementations of AuthService by default, tailored to suit a production and testing environment:

  1. SupabaseAuthService: For production use, integrated with Supabase's authentication API, configurable via the .env file (refer to App Flow - Setup).
  2. FauxAuthService: A lightweight mock service for testing and development, operating independently of any backend.

AuthService Provider

The AuthService implementation is determined dynamically based on the presence of Supabase credentials. If credentials are missing, the FauxAuthService is used as a fallback.

auth_service.dart
@Riverpod(keepAlive: true)
AuthService authService(AuthServiceRef ref) {
  final hasSupabaseCredentials =
      dotenv.isEveryDefined(['SUPABASE_URL', 'SUPABASE_ANON_KEY']);

  if (hasSupabaseCredentials) {
    // Implement Supabase Auth Service
    AppLogger.i('Supabase Auth Service Linked');
    return ref.read(supabaseAuthServiceProvider);
  } else {
    // Fallback to Faux Auth Service
    AppLogger.i('FauxAuthService Linked');
    return ref.read(fauxAuthServiceProvider);
  }
}

Supported Methods

The AuthService interface defines the following methods for user authentication:

Method Description
logIn Authenticates a user with email and password.
logOut Ends the user's session.
signUp Registers a new user.
isLoggedIn Checks if the user is currently authenticated.
onAuthStateChange Streams authentication state changes.
initializeAuthListener Sets up routing behavior based on auth state changes.

Authentication State Change Events

The AuthService interface includes an AuthState stream, which emits events corresponding to authentication state changes. The following events are emitted:

Event Description
INITIAL_SESSION Triggered when an initial session is detected.
PASSWORD_RECOVERY Triggered during password recovery flows.
SIGNED_IN Triggered when a user successfully signs in.
SIGNED_OUT Triggered when a user signs out.
TOKEN_REFRESHED Triggered when an authentication token is refreshed.
USER_UPDATED Triggered when user details are updated.
USER_DELETED Triggered when a user account is deleted.
MFA_CHALLENGE_VERIFIED Triggered when an MFA challenge is successfully verified.

SupabaseAuthService

The SupabaseAuthService is production-ready implementation of AuthService that is integrated with Supabase for authentication.

For more information, refer to the Supabase Documentation:

Below is a snippet of the implemented logIn function.

supabase_auth_service.md
@riverpod
class SupabaseAuthService extends _$SupabaseAuthService implements AuthService {
  final supabase.SupabaseClient _supabase = supabase.Supabase.instance.client;

  @override
  Future<AuthResponse> logIn(UserAuthData userAuth) async {
    final response = await _supabase.auth.signInWithPassword(
      email: userAuth.email,
      password: userAuth.password,
    );

    return _mapAuthResponse(response);
  }

FauxAuthService

The FauxAuthService provides a mock implementation of AuthService for testing purposes. It simulates user authentication without requiring a backend.

Below is a snippet of the implemented logIn function.

faux_auth_service.md
@riverpod
class FauxAuthService extends _$FauxAuthService implements AuthService {
  final StreamController<AuthState> _authStateController =
      StreamController<AuthState>.broadcast();

  AuthSession? _currentSession;

  @override
  Future<AuthResponse> logIn(UserAuthData userAuth) async {
    _currentSession = AuthSession(
      accessToken: 'faux_access_token',
      user: const AuthUser(
        id: 'faux_user_id',
        createdAt: 'faux_created_at',
      ),
    );

    // Mock a auth state change broadcast
    _authStateController.add(
      AuthState(event: AuthChangeEvent.signedIn, session: _currentSession),
    );

    return AuthResponse(session: _currentSession);
  }
}

Authentication and Navigation

Auth Listener

Changes to the authentication state dynamically control navigation. This is handled in the implementation of initializeAuthListener. In the below example, signed-in users are redirected to the home page, while signed-out users are sent to the authentication page.

@override
void initializeAuthListener() {
  final AppRouter appRouter = ref.read(appRouterProvider);

  onAuthStateChange().listen((authState) {
    switch (authState.event) {
      case AuthChangeEvent.initialSession:
        // If user is logged in, redirect to home page
        if (isLoggedIn()) {
          appRouter.replaceAll([const HomeRoute()]);
        }
        break;
      case AuthChangeEvent.signedIn:
        appRouter.replaceAll([const HomeRoute()]);
        break;
      case AuthChangeEvent.signedOut:
        appRouter.replaceAll([const AuthPageRoute()]);
        break;
      default:
        break;
    }
  });
}

Authentication Guards

Route guards, such as AuthGuard, ensure secure navigation based on authentication status. Unauthorized users are redirected to the login page.
For more information refer to App Flow | Routing

app_router.dart
class AuthGuard extends AutoRouteGuard {
  final ProviderRef ref;

  AuthGuard(this.ref);

  @override
  Future<void> onNavigation(resolver, router) async {
    final isLoggedIn = ref.read(authServiceProvider).isLoggedIn();
    if (isLoggedIn) {
      resolver.next(true);
    } else {
      await router.replaceAll([const AuthPageRoute()]);
    }
  }
}