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
CredentialsProviderthat talks to your API - A signed-in user with an access token and refresh token in the session
- A
StreamBuilderthat 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
JWT backend (recommended)
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']),
);
},
)
Variant — cookie session
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);
});
}