better_auth_flutter 0.1.0 copy "better_auth_flutter: ^0.1.0" to clipboard
better_auth_flutter: ^0.1.0 copied to clipboard

A Flutter client package for integrating with Better Auth - a secure authentication platform.

better_auth_flutter #

Flutter client SDK for Better Auth — email/password and social sign-in, session management, reactive auth state, and persistent cookie sessions.

Suggestions or contributions welcome — reach out at @ekaksh_janweja.

Features #

  • Email/password and username sign-up, sign-in, sign-out
  • Social sign-in via ID token (Google, Apple) or web redirect
  • Password reset, email verification, account linking
  • Session listing and revocation
  • Reactive auth state: authStateChanges, onAuthChange, BetterAuthBuilder
  • Persistent cookie sessions that survive restarts
  • Every call returns a sealed Result<T> — no thrown exceptions
  • Encrypted session storage by default (keychain/keystore)
  • Plugins: admin, anonymous, api key, bearer, email OTP, JWT, magic link, multi-session, one-time token, organization, phone, two-factor

Install #

dependencies:
  better_auth_flutter: ^0.1.0

Getting started #

Initialize before runApp:

import "package:better_auth_flutter/better_auth_flutter.dart";
import "package:flutter/material.dart";

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await BetterAuthFlutter.initialize(
    url: "http://localhost:3000/api/auth", // your Better Auth base URL
    enableLogging: kDebugMode,
  );

  runApp(const MyApp());
}
Parameter Default Purpose
url Better Auth base URL (usually …/api/auth)
dio new Dio Supply your own configured Dio
store SecureStorage Custom StorageInterface for cookie persistence
enableLogging false Internal diagnostics. Never logs cookies or headers
hydrateOnInit true Look up the stored session in the background on startup
mode AuthMode.cookie Cookie or bearer authentication
callbackUrlScheme null Custom URL scheme for browser-redirect social sign-in

Results #

Every API call returns a sealed Result<T> — nothing throws, including network failures and malformed server responses.

final result = await BetterAuthFlutter.client.signInEmail(
  email: "user@example.com",
  password: "hunter2",
);

switch (result) {
  case Success(:final data):
    print("Welcome ${data.user?.name}");
  case Failure(:final error):
    print("${error.code}: ${error.message}");
}

user is nullable: with two-factor enabled, a successful sign-in returns twoFactorRedirect: true and no user until the second factor is verified via the two_factor plugin.

BetterError carries Better Auth's own code (USER_NOT_FOUND, INVALID_EMAIL_OR_PASSWORD, …) verbatim. When a request fails without a structured response, it carries a transport code from BetterErrorCodes instead:

if (result case Failure(:final error)) {
  if (error.isNetworkError) return showRetry();
  if (error.isUnauthorized) return goToSignIn();
  showMessage(error.message);
}

Reactive auth state #

Gate your app on auth state instead of polling:

BetterAuthProvider(
  child: MaterialApp(
    home: BetterAuthBuilder(
      authenticated: (context, user) => HomeScreen(user: user),
      unauthenticated: (context) => const SignInScreen(),
    ),
  ),
)

BetterAuthBuilder shows a loading indicator while the stored session is being looked up, so the sign-in screen never flashes on a cold start. Override it with loading:, and handle failed lookups with error: (a failed lookup usually means the network was unreachable — the session may still be valid, which is why it is not the same as being signed out).

For finer control, listen to the stream. It replays the current state to every new listener, so subscribing late still tells you where things stand:

BetterAuthFlutter.authStateChanges.listen((state) {
  switch (state) {
    case AuthInitial() || AuthLoading(): showSplash();
    case Authenticated(:final user):    goHome(user);
    case Unauthenticated():             goToSignIn();
    case AuthError(:final error):       showRetry(error);
  }
});

Or BetterAuthFlutter.onAuthChange for a plain Stream<User?>. Read the state synchronously with BetterAuthFlutter.authState, and refresh it on demand with BetterAuthFlutter.refreshSession().

Auth state updates automatically on sign-in, sign-up, sign-out, session refresh, and session expiry (any 401).

Authentication #

Email and password #

final client = BetterAuthFlutter.client;

await client.signUpEmail(
  name: "Ada Lovelace",
  email: "ada@example.com",
  password: "hunter2",
);

await client.signInEmail(email: "ada@example.com", password: "hunter2");

await client.signOut();
await BetterAuthFlutter.clearCookies(); // clears the persisted session too

Social sign-in #

Obtain the ID token with the provider's own SDK, then hand it to Better Auth:

// Using google_sign_in
final googleUser = await GoogleSignIn(serverClientId: "…").signIn();
final auth = await googleUser!.authentication;

await BetterAuthFlutter.client.signInSocial(
  body: {
    "provider": "google",
    "idToken": {"token": auth.idToken},
  },
);

Web redirect

Other providers use a browser redirect. Pass callbackUrlScheme to initialize, register the matching scheme in your platform config (see below), then:

final result = await BetterAuthFlutter.signInWithProvider(
  provider: SocialProvider.github,
);

if (result case Success(:final data) when data.token != null) {
  // Exchange the one-time token for a session in this package's jar.
  await BetterAuthFlutter.client.oneTimeToken.verify(
    body: {"token": data.token},
  );
  await BetterAuthFlutter.refreshSession();
}

The cookie caveat. The system browser (ASWebAuthenticationSession, Chrome Custom Tabs) keeps its own cookie store, so a session cookie set during the redirect does not reach this package's Dio jar. A plain cookie flow can't pick the session up afterward. The reliable path is the token handoff above: point callbackURL at a server route that mints a one-time token and redirects to <scheme>://callback?token=…; signInWithProvider returns that token. Native ID-token sign-in (Google/Apple) has no such issue.

Sessions #

await client.getSession();
await client.listSessions();
await client.revokeSession(...);
await client.revokeOtherSessions();

BetterAuthFlutter.refreshSession() re-fetches the session and updates auth state. BetterAuthProvider calls it automatically when the app returns to the foreground (pass refreshOnResume: false to opt out). If your server runs deferSessionRefresh, the required follow-up POST is handled transparently.

Bearer authentication #

By default the SDK uses session cookies. For backends that cannot rely on cookies, switch to bearer mode — the token is captured from the set-auth-token response header after sign-in and sent as Authorization: Bearer <token>. Requires the bearer() plugin on your server.

await BetterAuthFlutter.initialize(
  url: "https://example.com/api/auth",
  mode: AuthMode.bearer,
  // Optional: persist the token across restarts (in-memory by default).
  tokenStorage: MySecureTokenStorage(),
);

With the default InMemoryTokenStorage, bearer sessions do not survive an app restart. Supply a TokenStorage<String> backed by secure storage to persist them. Keep BearerOptions.requireSignature in sync with your server's bearer({ requireSignature }) — a mismatch produces bare 401s.

Plugins #

Plugins live in their own libraries, so you only pay for what you import. Each requires the matching plugin enabled on your Better Auth server.

import "package:better_auth_flutter/plugins/jwt.dart";

final token = await BetterAuthFlutter.client.jwt.token();
Library Getter Server plugin
plugins/admin.dart client.admin admin()
plugins/email_otp.dart client.emailOtp emailOTP()
plugins/jwt.dart client.jwt jwt()
plugins/phone.dart client.phone phoneNumber()
plugins/two_factor.dart client.twoFactor twoFactor()
plugins/anonymous.dart client.anonymous anonymous()
plugins/magic_link.dart client.magicLink magicLink()
plugins/api_key.dart client.apiKey apiKey()
plugins/organization.dart client.organization organization()
plugins/multi_session.dart client.multiSession multiSession()
plugins/one_time_token.dart client.oneTimeToken oneTimeToken()
plugins/bearer.dart bearer()

Storage #

Session cookies persist through a StorageInterface. SecureStorage (keychain/keystore, via flutter_secure_storage) is the default on native platforms — session cookies are credentials, so they are encrypted at rest and chunked to fit iOS keychain limits.

HiveStorage and SharedPreferencesStorage are also bundled (both unencrypted) for cases where the secure backend is inconvenient — for example desktop platforms, where flutter_secure_storage needs native dependencies such as libsecret:

await BetterAuthFlutter.initialize(url: "…", store: SharedPreferencesStorage());

Or implement StorageInterface yourself:

class MyStorage implements StorageInterface {
  @override
  Future<void> saveCookies(String url, List<Cookie> cookies) async { … }

  @override
  Future<List<Cookie>> loadCookies(String url) async { … }

  @override
  Future<void> deleteCookies(String url) async { … }

  @override
  Future<void> deleteAll() async { … }
}

await BetterAuthFlutter.initialize(url: "…", store: MyStorage());

Sharing the session with your own HTTP client #

See doc/cookie-sharing.md.

final headers = await BetterAuthFlutter.getAuthHeaders();
final response = await http.get(myUrl, headers: headers);

Calling your own backend with a JWT #

The JWT plugin mints a short-lived token for your own services. Unlike Better Auth's rolling sessions, that token has a real expiry — which makes it the one place a refresh-on-401 interceptor such as fresh_dio genuinely fits:

import "package:fresh_dio/fresh_dio.dart";

myBackendDio.interceptors.add(
  Fresh.oAuth2(
    tokenStorage: InMemoryTokenStorage<OAuth2Token>(),
    refreshToken: (_, _) async {
      final result = await BetterAuthFlutter.client.jwt.token();
      return switch (result) {
        Success(:final data) => OAuth2Token(accessToken: data.token),
        Failure() => throw RevokeTokenException(),
      };
    },
  ),
);

Use this for your API, not for Better Auth itself. Better Auth has no OAuth2 refresh grant — its sessions are rolling (expiresIn / updateAge) and the server extends them on access, so this package needs no refresh machinery.

Platform setup #

Android, iOS, macOS, Windows, and Linux are supported. Web is not currently supported.

Browser-redirect social sign-in (signInWithProvider) uses flutter_web_auth_2, which needs a callback activity registered for your scheme in android/app/src/main/AndroidManifest.xml:

<activity
    android:name="com.linusu.flutter_web_auth_2.CallbackActivity"
    android:exported="true">
  <intent-filter android:label="flutter_web_auth_2">
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    <data android:scheme="YOUR_SCHEME" />
  </intent-filter>
</activity>

The scheme must match callbackUrlScheme in initialize. The native ID-token flow needs none of this.

Contributing #

flutter pub get
dart run build_runner build --delete-conflicting-outputs
flutter test

vigilant-spoon (a git submodule) is a runnable Better Auth server for local testing.

License #

MIT