Guides

Email / Password Auth

End-to-end guide for email and password authentication with a JWT backend.

This guide walks through a complete email/password setup with a JWT backend. By the end you'll have:

  • A configured CredentialsProvider that talks to your API
  • A signed-in user with an access token and refresh token in the session
  • A StreamBuilder that reacts to auth state changes
  • Unit tests that run without a network connection

Backend contract

Authyra doesn't care about your backend tech stack. It expects your sign-in endpoint to return a JSON body with at least:

{
  "id":           "usr_01j...",
  "email":        "alice@example.com",
  "name":         "Alice",
  "accessToken":  "eyJhbGciOi...",
  "refreshToken": "dGhpcyBpcyBh...",
  "expiresAt":    "2026-03-01T12:00:00.000Z"
}

If your backend doesn't return tokens (cookie session, opaque session), use CredentialsProvider instead of .withTokens — see the cookie session variant in step 2 below.


1. Install

pubspec.yaml
dependencies:
  authyra_flutter: ^0.1.0  # Flutter app
  # or authyra: ^0.1.0    # Dart backend / tests only

2. Configure the provider

lib/auth/auth_provider.dart
import 'package:authyra_flutter/authyra_flutter.dart';

CredentialsProvider.withTokens(
  id: 'email',
  authorize: (creds) async {
    final res = await myApi.post('/auth/login', body: {
      'email':    creds!['email'],
      'password': creds['password'],
    });

    if (res.statusCode == 401) return null; // wrong credentials
    if (res.statusCode != 200) {
      throw AuthenticationFailedException(
        'Login failed: HTTP ${res.statusCode}',
        providerName: 'email',
      );
    }

    return AuthSignInResult(
      user: AuthUser(
        id:    res.data['id'],
        email: res.data['email'],
        name:  res.data['name'],
      ),
      accessToken:  res.data['accessToken'],
      refreshToken: res.data['refreshToken'],
      expiresAt:    DateTime.parse(res.data['expiresAt']),
    );
  },
)

If your backend handles sessions server-side and only returns the user profile:

CredentialsProvider(
  id: 'email',
  authorize: (creds) async {
    final res = await myApi.post('/auth/login', body: creds);
    if (res.statusCode != 200) return null;
    return AuthUser(id: res.data['id'], email: res.data['email']);
  },
)

3. Initialize at startup

Flutter

lib/main.dart
import 'package:authyra_flutter/authyra_flutter.dart';

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

  await Authyra.initialize(
    client: AuthyraClient(
      providers: [myEmailProvider],
      storage: SecureAuthStorage(),
    ),
  );

  runApp(const MyApp());
}

Dart backend

bin/server.dart
import 'package:authyra/authyra.dart';

final client = AuthyraClient(
  providers: [myEmailProvider],
  storage: MyRedisStorage(),
);

await client.initialize();

4. Build the sign-in form

lib/pages/login_page.dart
import 'package:authyra_flutter/authyra_flutter.dart';
import 'package:flutter/material.dart';

class LoginPage extends StatefulWidget {
  const LoginPage({super.key});

  @override
  State<LoginPage> createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {
  final _email    = TextEditingController();
  final _password = TextEditingController();
  bool _loading   = false;
  String? _error;

  Future<void> _signIn() async {
    setState(() { _loading = true; _error = null; });
    try {
      await Authyra.instance.signIn('email', params: {
        'email':    _email.text.trim(),
        'password': _password.text,
      });
      // authStateChanges emits — your router navigates automatically
    } on AuthenticationFailedException {
      setState(() => _error = 'Invalid email or password.');
    } catch (e) {
      setState(() => _error = 'Something went wrong. Try again.');
    } finally {
      setState(() => _loading = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            TextField(
              controller: _email,
              decoration: const InputDecoration(labelText: 'Email'),
              keyboardType: TextInputType.emailAddress,
            ),
            const SizedBox(height: 12),
            TextField(
              controller: _password,
              decoration: const InputDecoration(labelText: 'Password'),
              obscureText: true,
            ),
            if (_error != null) ...[
              const SizedBox(height: 12),
              Text(_error!, style: const TextStyle(color: Colors.red)),
            ],
            const SizedBox(height: 24),
            FilledButton(
              onPressed: _loading ? null : _signIn,
              child: _loading
                  ? const CircularProgressIndicator()
                  : const Text('Sign in'),
            ),
          ],
        ),
      ),
    );
  }
}

5. React to auth state

StreamBuilder

lib/app.dart
import 'package:authyra_flutter/authyra_flutter.dart';
import 'package:flutter/material.dart';

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: StreamBuilder<AuthState>(
        stream: Authyra.instance.authStateChanges,
        builder: (context, snapshot) {
          final state = snapshot.data ?? AuthState.unauthenticated();
          return switch (state.type) {
            AuthStateType.authenticated   => HomePage(user: state.user!),
            AuthStateType.unauthenticated => const LoginPage(),
            AuthStateType.error           => ErrorPage(state.error!),
          };
        },
      ),
    );
  }
}

GoRouter

GoRouter(
  refreshListenable: StreamToListenable(Authyra.instance.authStateChanges),
  redirect: (context, routerState) {
    final authenticated = Authyra.instance.isAuthenticated;
    final goingToLogin  = routerState.uri.path == '/login';

    if (!authenticated && !goingToLogin) return '/login';
    if (authenticated  &&  goingToLogin) return '/home';
    return null;
  },
  routes: [
    GoRoute(path: '/login', builder: (_, __) => const LoginPage()),
    GoRoute(path: '/home',  builder: (_, __) => const HomePage()),
  ],
);

6. Access the token for API calls

final token = await Authyra.instance.getAccessToken();
myHttpClient.options.headers['Authorization'] = 'Bearer $token';

Or read it from the full session:

final session = await Authyra.instance.getSession();
print('Token expires: ${session?.expiresAt}');
print('User: ${session?.user.email}');

7. Sign out

await Authyra.instance.signOut();
// authStateChanges emits AuthState.unauthenticated() → router navigates to /login

Testing

No backend required — use InMemoryStorage and a fake authorize callback:

test/email_auth_test.dart
import 'package:test/test.dart';
import 'package:authyra/authyra.dart';

const _validEmail    = 'alice@example.com';
const _validPassword = 'secret';

AuthyraClient _makeClient() => AuthyraClient(
  providers: [
    CredentialsProvider.withTokens(
      id: 'email',
      authorize: (creds) async {
        if (creds?['email'] == _validEmail &&
            creds?['password'] == _validPassword) {
          return AuthSignInResult(
            user: AuthUser(id: '1', email: _validEmail),
            accessToken:  'fake-access',
            refreshToken: 'fake-refresh',
            expiresAt:    DateTime.now().add(const Duration(hours: 1)),
          );
        }
        return null;
      },
    ),
  ],
  storage: InMemoryStorage(),
);

void main() {
  late AuthyraClient client;

  setUp(() async {
    client = _makeClient();
    await client.initialize();
  });

  test('returns user on valid credentials', () async {
    final user = await client.signIn('email', params: {
      'email':    _validEmail,
      'password': _validPassword,
    });
    expect(user.email, _validEmail);
  });

  test('stores access token in session', () async {
    await client.signIn('email', params: {
      'email': _validEmail, 'password': _validPassword,
    });
    final token = await client.getAccessToken();
    expect(token, 'fake-access');
  });

  test('throws on wrong password', () {
    expect(
      () => client.signIn('email', params: {
        'email': _validEmail, 'password': 'wrong',
      }),
      throwsA(isA<AuthenticationFailedException>()),
    );
  });

  test('isAuthenticated is false after sign out', () async {
    await client.signIn('email', params: {
      'email': _validEmail, 'password': _validPassword,
    });
    await client.signOut();
    expect(await client.isAuthenticated(), isFalse);
  });
}

See also

Copyright © 2026