First Auth
Build a working sign-in from scratch in one page. You'll have a configured CredentialsProvider, a persisted session, and a reactive StreamBuilder before you reach the end.
This page uses a mocked authorize callback so you can follow along without a real backend. Swap the mock for your actual API call when you're ready.
1. Install
dependencies:
authyra_flutter: ^0.1.0
flutter pub get
2. Initialize at startup
import 'package:authyra_flutter/authyra_flutter.dart';
import 'package:flutter/material.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Authyra.initialize(
client: AuthyraClient(
providers: [
CredentialsProvider.withTokens(
id: 'email',
authorize: (creds) async {
// Replace this mock with your real API call:
// final res = await myApi.post('/auth/login', body: creds);
if (creds?['email'] == 'demo@example.com' &&
creds?['password'] == 'password') {
return AuthSignInResult(
user: AuthUser(id: '1', email: creds!['email'] as String, name: 'Demo User'),
accessToken: 'demo-access-token',
refreshToken: 'demo-refresh-token',
expiresAt: DateTime.now().add(const Duration(hours: 1)),
);
}
return null; // wrong credentials → throws AuthenticationFailedException
},
),
],
storage: SecureAuthStorage(),
config: const AuthConfig(autoRefresh: true),
),
);
runApp(const MyApp());
}
Authyra.initialize restores any previously persisted session, so returning users are authenticated immediately on next launch.
3. React to auth state
Wrap your root widget in a StreamBuilder on authStateChanges:
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 => Scaffold(
body: Center(child: Text('Error: ${state.error}')),
),
};
},
),
);
}
}
The StreamBuilder re-renders whenever the auth state changes — no manual navigation calls needed.
4. Build the login form
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,
});
// StreamBuilder above detects the new AuthState and renders HomePage.
} on AuthenticationFailedException {
setState(() => _error = 'Invalid email or password.');
} catch (_) {
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,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text('Sign in',
style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold)),
const SizedBox(height: 32),
TextField(
controller: _email,
decoration: const InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.emailAddress,
),
const SizedBox(height: 12),
TextField(
controller: _password,
decoration: const InputDecoration(
labelText: 'Password',
border: OutlineInputBorder(),
),
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 SizedBox.square(
dimension: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Sign in'),
),
],
),
),
);
}
}
5. Build the home page
import 'package:authyra_flutter/authyra_flutter.dart';
import 'package:flutter/material.dart';
class HomePage extends StatelessWidget {
final AuthUser user;
const HomePage({super.key, required this.user});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Hello, ${user.name ?? user.email ?? 'there'}!'),
actions: [
IconButton(
icon: const Icon(Icons.logout),
tooltip: 'Sign out',
onPressed: () => Authyra.instance.signOut(),
),
],
),
body: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('User ID: ${user.id}'),
if (user.email != null) Text('Email: ${user.email}'),
],
),
),
);
}
}
Authyra.instance.signOut() clears the session and emits AuthState.unauthenticated(). The StreamBuilder re-renders LoginPage automatically.
Try it
Run the app and sign in with:
- Email:
demo@example.com - Password:
password
Close the app and reopen it — the session is restored from SecureAuthStorage and you land directly on HomePage.
Connect to a real backend
Replace the mock authorize callback with your actual API:
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']),
);
},
Returning null causes signIn to throw AuthenticationFailedException. Throw it explicitly for non-credential errors (network, server 5xx).
Next steps
- Email Auth guide → — full walkthrough with tests and token access
- Google Sign-In → — add OAuth2 alongside email/password
- Configuration → — tune token lifetime and auto-refresh
- Full Flutter App Example →