Android login redirect issue
Closed this issue ยท 11 comments
Hello! I'm facing an issue with flutter_appauth. I have created a custom wrapper in a fresh demo app, similar to the one that keycloak_wrapper offers. This simplified version of the wrapper provides the MainApp class with a bool stream to conditionally render the screen's content. The auth provider I am trying to use is Keycloak.
When I tap the Login button, the chrome browser loads the login page correctly. The credentials I provide are valid. However, when I try to login, the page redirects me back to the app for a split second and opens up the browser window again, as you can see in the video: auth_bug.webm
I have tried the alternative in the guides and changing the Keycloak provider's settings to different values (i.e. single word for redirect uri). They all have the same result. I cannot figure out how to make this work. Considering I have followed the guide and this is a fresh app and it works normally on iOS, it seems like a bug. Please let me know if there is something I have missed.
// lib/main.dart
import 'package:auth/auth/wrapper.dart';
import 'package:flutter/material.dart';
final authWrapper = AuthWrapper();
void main() {
WidgetsFlutterBinding.ensureInitialized();
authWrapper.initialize();
runApp(const MainApp());
}
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: StreamBuilder<bool>(
stream: authWrapper.authenticationStream,
builder: (context, snapshot) {
final isLoggedIn = snapshot.data ?? false;
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(isLoggedIn ? 'Hello World!' : 'Welcome!'),
ElevatedButton(
onPressed:
isLoggedIn ? authWrapper.logout : authWrapper.login,
child: Text(isLoggedIn ? 'Logout' : 'Login'),
)
],
);
},
),
),
),
);
}
}
// lib/auth/wrapper.dart
import 'dart:async';
import 'dart:developer';
import 'package:flutter_appauth/flutter_appauth.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
const _appAuth = FlutterAppAuth();
const _secureStorage = FlutterSecureStorage();
const Map<String, String> authConfig = {
'clientId': 'domx_mobile_app',
'redirectUri': 'com.io.domx://auth',
'discoveryUrl': 'https://sso.domx-dev.com/auth/realms/domx'
'/.well-known/openid-configuration',
};
class AuthWrapper {
factory AuthWrapper() => _instance ??= AuthWrapper._();
AuthWrapper._();
static AuthWrapper? _instance = AuthWrapper._();
bool _isInitialized = false;
late final _streamController = StreamController<bool>();
/// The details from making a successful token exchange.
TokenResponse? tokenResponse;
/// Checks the validity of the token response.
bool get isTokenResponseValid =>
tokenResponse != null &&
tokenResponse?.accessToken != null &&
tokenResponse?.idToken != null;
/// The stream of the user authentication state.
///
/// Returns true if the user is currently logged in.
Stream<bool> get authenticationStream => _streamController.stream;
/// Whether this package has been initialized.
bool get isInitialized => _isInitialized;
/// Returns the id token string.
///
/// To get the payload, do `jwtDecode(KeycloakWrapper().idToken)`.
String? get idToken => tokenResponse?.idToken;
/// Returns the refresh token string.
///
/// To get the payload, do `jwtDecode(KeycloakWrapper().refreshToken)`.
String? get refreshToken => tokenResponse?.refreshToken;
void _assert() {
const message =
'Make sure the package has been initialized prior to calling this method.';
assert(_isInitialized, message);
}
/// Initializes the user authentication state and refresh token.
Future<void> initialize() async {
try {
final securedRefreshToken = await _secureStorage.read(
key: 'refreshToken',
);
if (securedRefreshToken == null) {
log('No refresh token is stored');
_streamController.add(false);
} else {
tokenResponse = await _appAuth.token(
TokenRequest(
authConfig['clientId']!,
authConfig['redirectUri']!,
discoveryUrl: authConfig['discoveryUrl']!,
refreshToken: securedRefreshToken,
),
);
await _secureStorage.write(
key: 'refreshToken',
value: refreshToken,
);
_streamController.add(isTokenResponseValid);
}
_isInitialized = true;
} catch (error) {
log(
'Initialization error',
name: 'auth_wrapper',
error: error,
);
}
}
/// Logs the user in.
Future<void> login() async {
_assert();
try {
tokenResponse = await _appAuth.authorizeAndExchangeCode(
AuthorizationTokenRequest(
authConfig['clientId']!,
authConfig['redirectUri']!,
discoveryUrl: authConfig['discoveryUrl']!,
scopes: ['openid', 'profile'],
promptValues: ['login'],
preferEphemeralSession: true,
),
);
if (isTokenResponseValid && refreshToken != null) {
await _secureStorage.write(
key: 'refreshToken',
value: tokenResponse!.refreshToken,
);
} else {
log('Invalid token response.');
}
_streamController.add(isTokenResponseValid);
} catch (error) {
log(
'Login error',
name: 'auth_wrapper',
error: error,
);
}
}
/// Logs the user out.
Future<void> logout() async {
_assert();
try {
await _appAuth.endSession(EndSessionRequest(
idTokenHint: idToken,
discoveryUrl: authConfig['discoveryUrl']!,
postLogoutRedirectUrl: authConfig['redirectUri']!,
preferEphemeralSession: true,
));
await _secureStorage.delete(key: 'refreshToken');
_streamController.add(false);
} catch (error) {
log(
'Login error',
name: 'auth_wrapper',
error: error,
);
}
}
}
My android app's build.gradle is configured according to the instructions,
//...
android {
//...
defaultConfig {
applicationId = "com.example.auth"
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutterVersionCode.toInteger()
versionName = flutterVersionName
manifestPlaceholders += ['appAuthRedirectScheme': 'com.io.domx'] // <- Added this line
}
//...
}
//...
Thank you in advance!
I'm facing similar issue with the default example code that is provided with flutter_appauth repo on an android device. error that can be observed from runnning the dev build with vscode is
W/AppAuth (19428): No stored state - unable to handle response
after logging in.
Same problem here.
W/AppAuth ( 4437): No stored state - unable to handle response
W/tions.scripting( 4437): Cleared Reference was only reachable from finalizer (only reported once)
I/flutter ( 4437): PlatformException(authorize_and_exchange_code_failed, Failed to authorize: [error: null, description: User cancelled flow], null, null)
same problem for me too for Android. any updates?
My new solution finally works, I compared it to another old solution and found that the new AndroidManifest.xml contains an empty property : android:taskAffinity=""
I remove it and everything works fine.
@leutbounpaseuth thanks for sharing! It works! The question now is why does it work?
@leutbounpaseuth thank you very much. After hours of searching and trying different solutions, this is the thing that finally worked !!
@leutbounpaseuth this works for me as well. KUDOS!
I was not able to solve this from days... really appreciate @leutbounpaseuth ... May god bless you with all happiness available in the universe
Since the Android Manifest is prefilled with the "android:taskAffinity=""
, by default
This should take place in the flutter_appauth documentation (Getting Started or Android Setup sections).
One sentence could save a lot of hours finding this thread.
My new solution finally works, I compared it to another old solution and found that the new AndroidManifest.xml contains an empty property :
android:taskAffinity=""
I remove it and everything works fine.
Thanks, It worked ๐
My new solution finally works, I compared it to another old solution and found that the new AndroidManifest.xml contains an empty property :
android:taskAffinity=""
I remove it and everything works fine.
my AndroidManifest.xml not contain android:taskAffinity=""
. But i have same problem