Migrate Flutter mobile app to Keycloak OIDC authentication (main + el-salvador branches)
Migrate Flutter Mobile App to Keycloak OIDC Authentication
Problem
The backend has been fully migrated to Keycloak OIDC authentication. The legacy auth endpoints (POST /api/auth/login, POST /api/auth/register, POST /api/auth/change-password, POST /api/auth/reset-password, PUT /api/users/email) no longer exist — the only remaining auth route is POST /api/auth/logout which clears ArangoDB sessions. All other auth is handled by Keycloak's Authorization Code flow.
The Vue 3 web frontend has been fully migrated and uses oidc-client-ts with Authorization Code + PKCE. Registration, password management, and email changes are delegated to Keycloak's built-in features (registration endpoint, Account Console).
The Flutter mobile app still uses the old legacy auth system. It calls endpoints that no longer exist, stores credentials insecurely, and has no OIDC integration. The app is completely broken for authentication on any deployment using the current backend.
This issue covers both:
-
mainbranch — GENIE.AI (multi-language,https://genie-ai.itu.int) -
el-salvadorbranch — AgroGenio AI (English + Spanish,https://ai.assembly.govstack.global)
Both branches share identical auth service code. The only differences are: API base URL, branding, language set, and the registration field name (username vs loginName — el-salvador already fixed this).
Current State (Broken)
What the mobile app currently does
| Feature | Current Implementation | Backend Status |
|---|---|---|
| Login |
POST /api/auth/login with {loginName, encPassword} (SHA-256 hashed) |
Endpoint removed |
| Registration |
POST /api/auth/register with {loginName, email, encPassword}
|
Endpoint removed |
| Token storage | In-memory only (ApiService._accessToken), lost on app restart |
N/A |
| Token refresh |
AuthProxy.refreshToken() method exists but never called
|
Endpoint removed |
| Password reset |
POST /api/auth/reset-password + /reset-password/confirm
|
Endpoint removed |
| Password change | Settings only offers reset-via-email; PasswordProxy.changePassword() exists but not wired |
Endpoint removed |
| Email change |
PUT /api/users/email with {email, password, userId}
|
Endpoint removed |
| "Remember Me" | Stores plaintext username + password in SharedPreferences | N/A |
| Social login | Google/Facebook buttons rendered but onPressed: () {} (no-ops) |
N/A |
| SSL |
badCertificateCallback always returns true
|
N/A |
Files requiring changes
| File | Purpose |
|---|---|
lib/services/api_service.dart |
HTTP client, base URL, auth headers |
lib/services/auth_proxy.dart |
Login, refresh, logout methods |
lib/services/user_service.dart |
Registration, email change, user CRUD |
lib/services/password_proxy.dart |
Password reset/change methods |
lib/components/auth/login_screen.dart |
Login UI, "Remember Me" logic |
lib/components/auth/register_screen.dart |
Registration UI |
lib/components/auth/registration_success_screen.dart |
Post-registration screen |
lib/components/auth/password_reset_initiate_screen.dart |
Password reset email entry |
lib/components/auth/password_reset_confirm_screen.dart |
New password entry |
lib/components/settings/settings_component.dart |
Email change, password change UI |
lib/main.dart |
SSL bypass, session state |
lib/services/genie_ai_config.dart |
Runtime config loading |
assets/config/genie-ai-config.json |
Deployment config (Keycloak settings) |
pubspec.yaml |
Dependencies |
Reference Implementation (Vue 3 Web Frontend)
The Vue 3 app is the reference. Here is exactly how it works:
Library
-
oidc-client-tsv3.5.0 — TypeScript OIDC client library
Flow
- Authorization Code + PKCE (standard for public clients)
-
response_type: 'code',scope: 'openid profile email roles'
Configuration (src/config/oidcConfig.js)
{
authority: `${keycloakUrl}/realms/${realm}`,
client_id: clientId,
redirect_uri: `${origin}/callback`,
post_logout_redirect_uri: origin,
response_type: 'code',
scope: 'openid profile email roles',
automaticSilentRenew: true,
storeAuthStateInCookie: false
}
Config hierarchy: runtime config (window.APP_CONFIG.keycloak) -> env vars -> defaults.
Token management
- In-memory only — never stored in localStorage/sessionStorage/cookies
- Automatic silent renew via hidden iframe (Keycloak's login-status-iframe)
-
Manual refresh on 401:
signinSilent()-> retry original request -> redirect to login if refresh fails
API interceptor (src/services/httpService.js)
- Request interceptor: attaches
Authorization: Bearer <token>to every request - Response interceptor: catches 401 -> calls
signinSilent()-> retries once -> redirects to login on failure
Registration
-
Delegated to Keycloak — uses Keycloak's registration endpoint:
{keycloakUrl}/realms/{realm}/protocol/openid-connect/registrations?client_id={clientId}&response_type=code&scope=openid+profile+email+roles - Old registration/verification screens were removed entirely
Password & email management
-
Delegated to Keycloak Account Console — a single "Manage my account" button opens:
{keycloakUrl}/realms/{realm}/account/ - Keycloak handles password change, password reset, email update, and email verification natively
Logout
- Calls
signoutRedirect({ id_token_hint })to Keycloak's end_session_endpoint - Sets
genie_post_logoutflag in sessionStorage to prevent auto re-login
User provisioning
- Backend auto-provisions users in ArangoDB on first authenticated request via
keycloak-auth-middleware.js - User data is sourced from Keycloak JWT claims (
sub,email,name,preferred_username,realm_access.roles)
Solution — Simplest Possible Approach
Use flutter_appauth for OIDC
The Flutter equivalent of oidc-client-ts is flutter_appauth — it implements Authorization Code + PKCE for both iOS and Android using native platform libraries (AppAuth).
What to build
1. Replace all auth services with a single OIDC service
Create lib/services/keycloak_auth_service.dart that wraps flutter_appauth:
import 'package:flutter_appauth/flutter_appauth.dart';
class KeycloakAuthService {
static final KeycloakAuthService _instance = KeycloakAuthService._();
factory KeycloakAuthService() => _instance;
KeycloakAuthService._();
final FlutterAppAuth _appAuth = FlutterAppAuth();
// Config loaded from genie-ai-config.json
late String authority; // e.g. https://genie-ai.itu.int/auth/realms/genie
late String clientId; // e.g. genie-app
late String redirectUrl; // e.g. com.itu.genieai:/callback (deep link)
late String postLogoutRedirectUrl;
Future<void> initialize(Map<String, dynamic> config) { ... }
Future<AuthTokenResponse?> login() { ... } // signinRedirect equivalent
Future<AuthTokenResponse?> handleCallback() { ... } // process auth code
Future<AuthTokenResponse?> refreshToken(String refreshToken) { ... }
Future<void> logout(String idToken) { ... } // end_session_endpoint
String? get accessToken => _currentToken?.accessToken;
String? get idToken => _currentToken?.idToken;
}
2. Replace ApiService interceptor with token attachment + 401 handling
// In ApiService:
Future<Response> get(String endpoint, ...) async {
final response = await _dio.get(...); // dio or http package
if (response.statusCode == 401 && !_isRefreshing) {
_isRefreshing = true;
final newToken = await KeycloakAuthService().refreshToken(_storedRefreshToken);
if (newToken != null) {
// Update stored token and retry
return get(endpoint, ...);
}
// Refresh failed -> redirect to login
}
return response;
}
3. Store refresh token securely (for session persistence)
Use flutter_secure_storage (already in pubspec.yaml but unused) to persist the refresh token:
- On successful login: store refresh token
- On app startup: check for stored refresh token -> attempt silent refresh -> restore session
- On logout: clear secure storage
4. Delete all legacy auth files and screens
Remove entirely:
-
lib/services/auth_proxy.dart— replaced bykeycloak_auth_service.dart -
lib/services/password_proxy.dart— password management delegated to Keycloak Account Console -
lib/components/auth/register_screen.dart— registration delegated to Keycloak -
lib/components/auth/registration_success_screen.dart— no longer needed -
lib/components/auth/password_reset_initiate_screen.dart— no longer needed -
lib/components/auth/password_reset_confirm_screen.dart— no longer needed -
lib/services/user_service.dart— gut it: keep onlygetCurrentUserInfo(),updateAccountSettings(),deleteAccount(),deactivateAccount()(these still use authenticated API calls, not auth-specific)
Modify:
-
lib/components/auth/login_screen.dart— replace login form with a single "Sign in with Keycloak" button that callsKeycloakAuthService().login(). Remove username/password fields, "Remember Me" checkbox, social login buttons, and SHA-256 hashing. -
lib/components/settings/settings_component.dart— replace email change UI and password change UI with a single "Manage my account" button that opens Keycloak Account Console URL ({authority}/account/) in the system browser (same pattern as Vue 3'swindow.open()). -
lib/main.dart— removeMyHttpOverridesSSL bypass (or gate behind debug mode). Add OIDC callback handling on app startup. On cold start, attempt to restore session from stored refresh token.
5. Add Keycloak config to genie-ai-config.json
Both branches already load runtime config from assets/config/genie-ai-config.json. Add a keycloak section:
{
"title": "GENIE.AI",
"keycloak": {
"url": "https://genie-ai.itu.int/auth",
"realm": "genie",
"clientId": "genie-app"
}
}
For el-salvador:
{
"title": "AgroGenio AI",
"keycloak": {
"url": "https://ai.assembly.govstack.global/auth",
"realm": "genie",
"clientId": "genie-app"
}
}
6. Configure deep link redirect URI
Add a custom URL scheme to both platforms for the OAuth callback:
android/app/src/main/AndroidManifest.xml:
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="com.itu.genieai" android:path="/callback" />
</intent-filter>
ios/Runner/Info.plist:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>com.itu.genieai</string>
</array>
</dict>
</array>
For el-salvador, use a separate scheme: com.itu.genieai.sv (or configure per-deployment via genie-ai-config.json).
The redirect URI must also be registered as a Valid Redirect URI in the Keycloak client configuration for genie-app.
7. Update pubspec.yaml dependencies
dependencies:
flutter_appauth: ^6.0.0 # OIDC Authorization Code + PKCE
flutter_secure_storage: ^9.0.0 # Already exists, just needs usage
# Remove: crypto (no more client-side SHA-256 hashing)
What NOT to build
- No custom registration screen — Keycloak handles it
- No password reset/change screens — Keycloak Account Console handles it
- No email change screen — Keycloak Account Console handles it
- No username/email availability checks — Keycloak handles uniqueness
- No SHA-256 password hashing — passwords never touch the app
- No "Remember Me" checkbox — refresh token persistence replaces it
- No social login buttons — configure social identity providers in Keycloak directly
Branch-Specific Notes
main branch
- Base URL:
https://genie-ai.itu.int/api - Keycloak URL:
https://genie-ai.itu.int/auth - Deep link scheme:
com.itu.genieai - 15 languages supported
- Remove registration payload field name
username(should already beloginName)
el-salvador branch
- Base URL:
https://ai.assembly.govstack.global/api - Keycloak URL:
https://ai.assembly.govstack.global/auth - Deep link scheme:
com.itu.genieai.sv - 2 languages (English + Spanish only)
- Registration field name already fixed to
loginName - Branding: "AgroGenio AI"
Merge strategy
Implement on main first, then cherry-pick or merge into el-salvador. The auth code is identical between branches — only config values differ.
Acceptance Criteria
Both branches
-
Login via Keycloak Authorization Code + PKCE works -
OAuth callback via deep link works on both iOS and Android -
Access token attached to all API requests via Authorization: Bearerheader -
Token refresh on 401 works (silent refresh -> retry -> redirect to login) -
Session persists across app restarts via secure refresh token storage -
Logout calls Keycloak end_session_endpoint and clears local storage -
"Manage my account" opens Keycloak Account Console for password/email changes -
Old login form, registration screens, password reset screens removed -
No plaintext credentials stored anywhere -
No SHA-256 client-side password hashing -
No SSL certificate bypass in production builds
main branch
-
All 15 languages still supported -
Keycloak config loaded from genie-ai-config.json
el-salvador branch
-
English + Spanish languages supported -
AgroGenio AI branding preserved -
Keycloak config loaded from genie-ai-config.json -
Separate deep link scheme for el-salvador deployment
Related Issues
- Supersedes: #526 (closed) (token refresh lifecycle — the entire old auth system is being replaced)
- Depends on: Keycloak being deployed and configured (already done)
- Blocks: #597 (SSE streaming for Flutter — needs working auth first)
- Related: #598 (microservice auth middleware — backend must validate the Keycloak tokens the mobile app sends)