I was curious how I would be able to allow a user that just signed out of my app to click the Sign In
button and have a screen like below appear.
When a user signs out of my app, it requires them to have to re-enter their Microsoft credentials from scratch, whereas the app that I’m trying to mimic has somehow stored your previously signed in accounts and remembered as pictured above.
I’m writing my app using Flutter and the package for authentication that I’m using is aad_oauth
and this is what I have for the configuration of my AadOauth
instance.
auth_service.dart
static final Config config = Config(
tenant: '...',
clientId: '...',
scope: "${dotenv.env['AppScope']!} openid profile offline_access user.read",
redirectUri: (Platform.isAndroid)
? dotenv.env['androidRedirectUri']!
: dotenv.env['iOSRedirectUri'],
navigatorKey: navigatorKey,
// webUseRedirect: true, // default is false - on web only, forces a redirect flow instead of popup auth
loader: const Center(child: CircularProgressIndicator()),
// postLogoutRedirectUri: 'http://your_base_url/logout', //optional
);
final AadOAuth oauth = AadOAuth(config);
class AuthService extends ChangeNotifier {
// TODO: Should move the clientId + tenantId to .env file?
static final Config config = Config(
tenant: '...',
clientId: '...',
scope: "${dotenv.env['AppScope']!} openid profile offline_access user.read",
redirectUri: (Platform.isAndroid)
? dotenv.env['androidRedirectUri']!
: dotenv.env['iOSRedirectUri'],
navigatorKey: navigatorKey,
// webUseRedirect: true, // default is false - on web only, forces a redirect flow instead of popup auth
loader: const Center(child: CircularProgressIndicator()),
// postLogoutRedirectUri: 'http://your_base_url/logout', //optional
);
final AadOAuth oauth = AadOAuth(config);
bool _isLoadingUser = true;
bool get isLoadingUser => _isLoadingUser;
set setIsLoadingUser(bool value) {
_isLoadingUser = value;
notifyListeners();
}
bool _isAuthenticated = false;
bool get isAuthenticated => _isAuthenticated;
AuthService();
Future<void> initializeAuthService() async {
try {
if (await hasCachedAccountInformation()) {
log('User is already authenticated');
await oauth.refreshToken();
_isAuthenticated = true;
} else {
log('User is not authenticated');
}
} catch (e) {
log('Error initializing AuthService: $e');
} finally {
_isLoadingUser = false;
notifyListeners();
}
}
// Changed to Future<void>, see: https://www.reddit.com/r/dartlang/comments/maxl6y/futurevoid_versus_void_for_async_function_return/
Future<void> login() async {
final result = await oauth.login();
result.fold(
(l) => log(l.toString()),
(r) => log('Logged in successfully, your access token: $r'),
);
var accessToken = await oauth.getAccessToken();
if (accessToken != null) {
log('Access token: $accessToken'); // TODO: Delete?
_isLoadingUser = false;
_isAuthenticated = true;
notifyListeners();
}
}
Future<void> logout() async {
try {
log('Logging out'); // TODO: Delete
await oauth.logout();
_isAuthenticated = false;
notifyListeners();
} catch (e) {
log('Error logging out: ${e.toString()}');
}
}
Future<bool> hasCachedAccountInformation() async {
var hasCachedAccountInformation = await oauth.hasCachedAccountInformation;
log('HasCachedAccountInformation: $hasCachedAccountInformation'); // TODO: Delete
return hasCachedAccountInformation;
}
Future<String?> getCachedAccessToken() async {
var accessToken = await oauth.getAccessToken();
log('Got Access token: $accessToken'); // TODO: Delete
return accessToken;
}
refreshToken() async {
return await oauth.refreshToken();
}
}
auth_gate.dart (file that shows the Sign in page)
class AuthGate extends StatelessWidget {
AuthGate({super.key});
...
@override
Widget build(BuildContext context) {
...
// User is not authenticated
return LayoutBuilder(builder: (context, constraints) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
...
ElevatedButton(
onPressed: () async {
await showAlertDialog(
context, authService, isMobileWidth);
},
child: Text(
'SIGN IN',
style: isMobileWidth
? primaryButtonText
: primaryButtonText.copyWith(
fontSize: tabletFontSize,
),
),
),
],
});
}
showAlertDialog(
...
// set up the buttons
Widget cancelButton = TextButton(
child: Text(
"Cancel",
style: bodyRegular.copyWith(
fontSize: isMobileWidth ? mobileFontSize : tabletFontSize),
),
onPressed: () => Navigator.of(context).pop(), // dismiss dialog,
);
Widget continueButton = TextButton(
child: Text(
"Continue",
style: bodyRegular.copyWith(
fontSize: isMobileWidth ? mobileFontSize : tabletFontSize),
),
onPressed: () async {
authService.setIsLoadingUser = true;
Navigator.of(context).pop(); // dismiss dialog
await authService.login();
authService.setIsLoadingUser = false;
},
);
// Android Alert Dialog
AlertDialog androidAlert = AlertDialog(
title: Text(
alertDialogTitle,
style: bodyRegular.copyWith(
fontSize: isMobileWidth
? mobileDialogTitleFontSize
: tabletDialogTitleFontSize,
),
),
content: Text(
alertDialogContent,
style: bodyRegular.copyWith(
fontSize: isMobileWidth
? mobileDialogDescriptionFontSize
: tabletDialogDescriptionFontSize,
),
),
actions: [
cancelButton,
continueButton,
],
);
// iOS Alert Dialog
CupertinoAlertDialog iOSAlert = CupertinoAlertDialog(
title: Text(
alertDialogTitle,
style: bodyRegular.copyWith(
fontSize: isMobileWidth
? mobileDialogTitleFontSize
: tabletDialogTitleFontSize,
),
),
content: Text(
alertDialogContent,
style: bodyRegular.copyWith(
fontSize: isMobileWidth
? mobileDialogDescriptionFontSize
: tabletDialogDescriptionFontSize,
),
),
actions: [
cancelButton,
continueButton,
],
);
// show the dialog
showDialog(
context: context,
builder: (BuildContext context) {
if (Platform.isIOS) {
return iOSAlert;
} else {
return androidAlert;
}
},
);
}
}
2