Authentication with the Dart SDK
EthioConnect authenticates client requests with a short-lived JWT sent as a Bearer token. The commbaas SDK never stores tokens — it obtains one whenever it needs one, through the async tokenProvider closure you pass to CommBaasClient. How your app produces that token depends on your integration model.
Two token models
1. Backend-minted tokens (recommended for embedded apps)
Your backend holds the tenant API key and mints tokens on behalf of your own users. The Flutter app never sees the API key — it asks your backend for a token and returns it from the tokenProvider:
import 'package:commbaas/commbaas.dart';
final client = CommBaasClient(
baseUrl: 'https://api.example.com/v1',
tokenProvider: () async {
// Your own backend endpoint, protected by your own app auth.
return myBackend.fetchEthioConnectToken();
},
);
The SDK calls the provider whenever it needs a fresh JWT and never caches the result, so your closure is the single source of truth for credentials. Cache and refresh inside the closure as you see fit.
mintToken on the clientThe server-to-server mint endpoint requires the tenant API key, which must never ship in a client app. The client SDK therefore deliberately has no mintToken method — minting belongs to your backend (e.g. via @comm-baas/server-sdk). See ADR 0022 §6.
2. End-user credentials (platform-managed users)
If your users are registered directly with the platform, use the client.auth facade. login, register, and refresh do not require a Bearer token:
final res = await client.auth.login(
LoginRequest(email: email, password: password),
);
// res.user -> User (id, tenantId, email, displayName, platformRole, status, createdAt)
// res.accessToken -> short-lived JWT
// res.refreshToken -> long-lived refresh token (rotated on refresh)
Register a new user:
final res = await client.auth.register(RegisterRequest(
tenantId: tenantId,
email: email,
password: password,
displayName: 'Alice',
));
Wire the resulting tokens back into the SDK through a small token manager that backs the tokenProvider:
class TokenManager {
TokenManager(this.client);
final CommBaasClient client;
String? _accessToken;
String? _refreshToken;
/// Use as the CommBaasClient tokenProvider.
Future<String> getAccessToken() async {
_accessToken ??= await _loadFromStorageOrLogin();
return _accessToken!;
}
/// Call on AuthenticationException (401), then retry the request once.
Future<String> forceRefresh() async {
final res = await client.auth.refresh(
RefreshRequest(refreshToken: _refreshToken!),
);
_accessToken = res.accessToken;
_refreshToken = res.refreshToken; // rotated — store the new one
await _storeRefreshToken(_refreshToken!);
return _accessToken!;
}
}
Failed API calls throw the SDK's typed exceptions — catch AuthenticationException (401) to trigger a refresh, and treat a 401 from refresh itself as "session expired, force re-login".
Logout and profile
await client.auth.logout(); // revokes the current session's refresh token
final me = await client.auth.me(); // User — hydrate the UI after app restart
Delete your stored refresh token after logout().
Secure storage
The SDK never persists tokens — storage is entirely your app's concern (ADR 0022 §3). Store the refresh token with flutter_secure_storage, which uses the iOS Keychain and Android Keystore:
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
const _storage = FlutterSecureStorage();
Future<void> _storeRefreshToken(String token) =>
_storage.write(key: 'ec_refresh_token', value: token);
Future<String?> _loadRefreshToken() =>
_storage.read(key: 'ec_refresh_token');
Guidelines:
- Keep the access token in memory only. It is short-lived by design; persisting it adds risk without benefit.
- Flutter web:
flutter_secure_storagehas no secure equivalent in the browser (its web implementation is not hardware-backed). On web, prefer the backend-minted model — keep tokens in memory and re-fetch from your backend session on page load, exactly as the JavaScript client SDK's async token provider does. - Never embed the tenant API key (
cb_<prefix>.<secret>) in a Flutter app, including web builds.
Under the hood
The client.auth facade and the tokenProvider sit on top of these auth-service endpoints:
Server-to-server mint (your backend only, never the app):
POST /v1/auth/token
Authorization: ApiKey cb_<prefix>.<secret>
{ "user_id": "usr_123", "display_name": "Alice" }
200 → { "access_token": "...", "expires_in": 900 }
End-user flows (what the facade methods call):
POST /v1/auth/register— bodytenant_id,email,password,display_name; returns201withuser,access_token,refresh_token(409if the email is taken). →auth.register()POST /v1/auth/login— bodyemail,password; returnsuser,access_token,refresh_token(401on bad credentials). →auth.login()POST /v1/auth/refresh— bodyrefresh_token; returns a rotated pair (401means force re-login). →auth.refresh()POST /v1/auth/logout— Bearer auth; returns204. →auth.logout()GET /v1/auth/me— Bearer auth; returns the currentUser. →auth.me()
The SDK attaches Authorization: Bearer <JWT> to every authenticated request, calling your tokenProvider each time.
Verifying tokens (backend note): platform JWTs are signed with EdDSA (Ed25519). Your backend can verify them against the JWKS document at GET /.well-known/jwks.json. Flutter clients never need to verify tokens — they only present them.