Bapp Auth
Cross-platform Flutter authentication for BAPP framework platforms. Handles Keycloak SSO, automatic token management, and provides a pre-configured API client via bapp_api_client.
Features
- One-line setup -
BappAuthorchestrates Keycloak auth, token lifecycle, and API client - Automatic SSO - Silent login via existing Keycloak browser session using
prompt=none - Token management - Persistence, proactive refresh, 498 retry, concurrent refresh prevention
- Multi-app support - Storage scoped by
clientIdso multiple BAPP apps coexist on one device - Shared device support -
sessionIdscoping for kiosk/POS where multiple users share a device - All platforms - Android, iOS, macOS, Web, Windows, Linux
- Reactive state -
authStateStreamfor driving UI
Installation
dependencies:
bapp_auth:
git:
url: https://git.pidginhost.net/bapp-cloud/packages/flutter-bapp-auth.git
Quick Start
import 'package:bapp_auth/bapp_auth.dart';
final auth = BappAuth(
config: BappAuthConfig(
clientId: 'my-flutter-app', // Keycloak client ID
host: 'https://my-company.bapp.ro/api', // BAPP API host (per project)
app: 'my-app', // App slug
),
);
// Initialize: restores saved token, refreshes if expired, attempts SSO if enabled
await auth.initialize();
// If not authenticated, trigger interactive login
if (!auth.isAuthenticated) {
await auth.loginWithSSO();
}
// Use the pre-configured API client - auth is handled automatically
final me = await auth.apiClient.me();
final items = await auth.apiClient.list('myapp.item');
Configuration
BappAuthConfig controls all behavior. The Keycloak hostname (id.bapp.ro) is fixed; the API host is configurable per project.
BappAuthConfig(
// Required
clientId: 'my-flutter-app',
// BAPP API (configurable per project)
host: 'https://my-company.bapp.ro/api', // default: https://panel.bapp.ro/api
app: 'my-app', // default: account
// Keycloak (fixed)
keycloakHostname: 'id.bapp.ro', // fixed, cannot change
realm: 'bapp', // default: bapp
scopes: ['openid', 'profile', 'email'], // default scopes
// SSO behavior
ssoAutoLogin: true, // attempt silent SSO on initialize()
redirectUri: null, // auto-detected per platform if null
customScheme: null, // custom URL scheme (e.g. com.myapp.auth)
clientSecret: null, // for confidential clients only
// Shared device support
sessionId: null, // user-scoped storage for kiosk/POS
)
Authentication
SSO Login (Authorization Code Flow with PKCE)
Uses the system browser. If the user is already logged in to Keycloak, they're authenticated without seeing a login form.
// Interactive login - shows browser with login form if no session
await auth.loginWithSSO();
// Force fresh login (private/incognito mode)
await auth.loginWithSSO(preferEphemeral: true);
Device Flow
For devices without a browser (TV, IoT) or for cross-device auth.
await auth.loginWithDevice(
onUserAction: (userCode, verificationUri) {
// Show this to the user
print('Go to $verificationUri and enter: $userCode');
},
onStatusUpdate: (status) {
print(status); // "Waiting for user authorization..."
},
);
Logout
await auth.logout(); // Revokes Keycloak session + clears local token
Auth State
Listen reactively to auth state changes:
auth.authStateStream.listen((state) {
switch (state.status) {
case AuthStatus.initial:
// Not yet initialized
case AuthStatus.loading:
// Auth operation in progress
case AuthStatus.authenticated:
// Ready - state.token has the token
case AuthStatus.unauthenticated:
// Need login - state.error has the reason
}
});
Automatic SSO (Silent Login)
When ssoAutoLogin: true, initialize() attempts a silent SSO login if no saved token is found. This uses prompt=none so Keycloak returns immediately:
- If the user has an active Keycloak session: authenticated silently, no visible UI.
- If no session: Keycloak returns
login_requiredinstantly, falls through to unauthenticated.
Platform behavior
| Platform | Behavior |
|---|---|
| Web | A popup opens and closes almost instantly |
| Mobile | System browser opens and closes quickly. On iOS, first use shows "Allow X to sign you in" dialog |
| Desktop | Browser opens and closes quickly |
Recommendation
ssoAutoLogin defaults to false. Enable it when:
- Your Keycloak client is configured for SSO
- The brief browser flash is acceptable for your UX
- On web, it's generally safe since the popup is barely visible
BappAuthConfig(
clientId: 'my-app',
ssoAutoLogin: true, // enable for all platforms
// Or conditionally:
// ssoAutoLogin: PlatformConfig.isWeb, // only on web
)
Initialization Flow
When auth.initialize() is called:
- Load saved token from local storage (scoped by
clientId+sessionId) - If valid (not expired) - authenticate immediately
- If expired - attempt token refresh via Keycloak
- If refresh fails and
ssoAutoLogin: true- attempt silent SSO withprompt=none - Otherwise - transition to unauthenticated
Token refresh and saved token restoration are invisible to the user. Only the SSO auto-login step involves a brief browser interaction.
API Client
After authentication, auth.apiClient is a pre-configured BappApiClient with automatic bearer token injection and refresh.
// User profile
final me = await auth.apiClient.me();
// CRUD
final items = await auth.apiClient.list('myapp.item');
final item = await auth.apiClient.get('myapp.item', '42');
final created = await auth.apiClient.create('myapp.item', {'name': 'New'});
await auth.apiClient.patch('myapp.item', '42', {'name': 'Updated'});
await auth.apiClient.delete('myapp.item', '42');
// Tasks
final tasks = await auth.apiClient.listTasks();
final result = await auth.apiClient.runTask('myapp.export', {'format': 'csv'});
final asyncResult = await auth.apiClient.runTaskAsync('myapp.long_export');
// Documents
final url = auth.apiClient.getDocumentUrl(record, output: 'pdf');
final bytes = await auth.apiClient.getDocumentContent(record, output: 'pdf');
Multi-tenancy
// Set tenant for all subsequent requests
auth.setTenant('42');
// Change app slug
auth.setApp('erp');
Automatic Token Refresh
All requests through auth.apiClient are transparently authenticated:
- Proactive refresh: Before each request, if the token is expiring within 30 seconds, it's refreshed automatically.
- 498 retry: If Keycloak returns 498 (Token Expired), the token is refreshed and the request retried once.
- Concurrent prevention: Multiple simultaneous requests that trigger refresh only refresh once (lock-based).
- Auth required callback: If refresh fails, the auth state transitions to
unauthenticatedso the UI can show a login screen.
Shared Device Support (Kiosk / POS)
On shared devices where multiple users log in on the same app, use sessionId to scope token storage per user. Each user gets their own saved token that won't be overwritten by another user.
Storage key format
| Config | Storage key |
|---|---|
clientId: 'pos' |
bapp_auth_token_pos |
clientId: 'pos', sessionId: 'user-a' |
bapp_auth_token_pos_user-a |
clientId: 'pos', sessionId: 'user-b' |
bapp_auth_token_pos_user-b |
Setting sessionId at creation
final auth = BappAuth(
config: BappAuthConfig(
clientId: 'pos-terminal',
sessionId: 'cashier-1', // Scoped to this user
),
);
await auth.initialize(); // Loads cashier-1's saved token
Switching users at runtime
Use switchSession() to change users without recreating BappAuth:
final auth = BappAuth(
config: BappAuthConfig(clientId: 'pos-terminal'),
);
// Cashier A starts shift
await auth.switchSession('cashier-a');
await auth.loginWithSSO();
// ... cashier A works ...
// Cashier B takes over
await auth.switchSession('cashier-b');
// If cashier B logged in before, their token is restored.
// Otherwise, auth.isAuthenticated == false and they need to log in.
if (!auth.isAuthenticated) {
await auth.loginWithSSO();
}
switchSession() will:
- Logout the current user (revoke Keycloak session)
- Switch to the new session's storage key
- Re-initialize (load saved token, attempt refresh, SSO as configured)
Platform Configuration
Each platform requires specific configuration for OAuth redirects. PlatformConfig auto-detects the correct redirect URI:
final redirectUri = PlatformConfig.getRedirectUri();
// Android/iOS/macOS: com.bapp.auth://callback
// Web: https://yourdomain.com/auth/callback
// Windows/Linux: http://localhost:8080/auth/callback
Platform helpers
PlatformConfig.isWeb; // true on web
PlatformConfig.platformName; // "Android", "iOS", "Web", etc.
PlatformConfig.supportsCustomScheme; // false on web, Windows, Linux
PlatformConfig.getConfigurationInstructions(); // setup guide for current platform
For detailed platform-specific setup instructions, see PLATFORM_CONFIGURATION.md.
Direct Keycloak Access
For advanced use cases, the underlying KeycloakAuth is available:
final keycloak = auth.keycloakAuth;
// Direct token refresh
final newToken = await keycloak.refreshToken(refreshToken);
// Direct logout
await keycloak.logout(refreshToken: refreshToken);
// Device auth flow control
final deviceAuth = await keycloak.initiateDeviceAuth();
final token = await keycloak.pollForDeviceToken(
deviceCode: deviceAuth.deviceCode,
interval: deviceAuth.interval,
);
Example
See the example directory for a complete demo app.
License
MIT License
Libraries
- bapp_auth
- Authentication package for BAPP framework platforms.