202 lines
5.6 KiB
Dart
202 lines
5.6 KiB
Dart
import 'dart:async';
|
|
import 'dart:convert';
|
|
|
|
import 'package:google_sign_in_all_platforms/google_sign_in_all_platforms.dart';
|
|
import 'package:http/http.dart' as http;
|
|
|
|
import '../config/auth_config.dart';
|
|
import '../firebase_options.dart';
|
|
import '../models/app_user.dart';
|
|
|
|
class AuthServiceLinux {
|
|
AuthServiceLinux._();
|
|
|
|
static final AuthServiceLinux instance = AuthServiceLinux._();
|
|
|
|
final StreamController<AppUser?> _authController =
|
|
StreamController<AppUser?>.broadcast();
|
|
|
|
GoogleSignIn? _googleSignIn;
|
|
AppUser? _currentUser;
|
|
String? _idToken;
|
|
String? _refreshToken;
|
|
DateTime? _tokenExpiry;
|
|
|
|
Stream<AppUser?> get authStateChanges => _authController.stream;
|
|
|
|
AppUser? get currentUser => _currentUser;
|
|
|
|
Future<void> initialize() async {
|
|
if (!_hasOAuthConfig) {
|
|
_emitUser(null);
|
|
return;
|
|
}
|
|
|
|
_googleSignIn = GoogleSignIn(
|
|
params: GoogleSignInParams(
|
|
clientId: googleWebOAuthClientId!,
|
|
clientSecret: googleOAuthDesktopClientSecret!,
|
|
scopes: const <String>['openid', 'profile', 'email'],
|
|
),
|
|
);
|
|
|
|
_googleSignIn!.authenticationState.listen((GoogleSignInCredentials? creds) {
|
|
if (creds == null) {
|
|
_emitUser(null);
|
|
}
|
|
});
|
|
|
|
final GoogleSignInCredentials? restored =
|
|
await _googleSignIn!.silentSignIn();
|
|
if (restored?.idToken != null) {
|
|
try {
|
|
final AppUser user = await _signInToFirebaseWithIdp(restored!.idToken!);
|
|
_emitUser(user);
|
|
} catch (_) {
|
|
// Ignore stale desktop sessions on startup.
|
|
}
|
|
}
|
|
}
|
|
|
|
bool get _hasOAuthConfig =>
|
|
googleWebOAuthClientId != null &&
|
|
googleOAuthDesktopClientSecret != null;
|
|
|
|
Future<void> signInWithGoogle() async {
|
|
_validateOAuthConfig();
|
|
final GoogleSignInCredentials? credentials = await _googleSignIn!.signIn();
|
|
if (credentials == null) {
|
|
throw StateError('Google Sign-In was cancelled.');
|
|
}
|
|
|
|
final String? idToken = credentials.idToken;
|
|
if (idToken == null) {
|
|
throw StateError('Google Sign-In did not return an ID token.');
|
|
}
|
|
|
|
final AppUser user = await _signInToFirebaseWithIdp(idToken);
|
|
_emitUser(user);
|
|
}
|
|
|
|
Future<void> signOut() async {
|
|
await _googleSignIn?.signOut();
|
|
_clearTokens();
|
|
_emitUser(null);
|
|
}
|
|
|
|
Future<String?> getIdToken({bool forceRefresh = false}) async {
|
|
if (_currentUser == null) {
|
|
return null;
|
|
}
|
|
if (!forceRefresh &&
|
|
_idToken != null &&
|
|
_tokenExpiry != null &&
|
|
DateTime.now().isBefore(_tokenExpiry!)) {
|
|
return _idToken;
|
|
}
|
|
if (_refreshToken != null) {
|
|
await _refreshIdToken();
|
|
return _idToken;
|
|
}
|
|
return _idToken;
|
|
}
|
|
|
|
void _emitUser(AppUser? user) {
|
|
_currentUser = user;
|
|
if (user == null) {
|
|
_clearTokens();
|
|
}
|
|
_authController.add(user);
|
|
}
|
|
|
|
void _clearTokens() {
|
|
_idToken = null;
|
|
_refreshToken = null;
|
|
_tokenExpiry = null;
|
|
}
|
|
|
|
void _storeTokens(Map<String, dynamic> data) {
|
|
_idToken = data['idToken'] as String?;
|
|
_refreshToken = data['refreshToken'] as String?;
|
|
final int expiresIn = (data['expiresIn'] as String?) != null
|
|
? int.parse(data['expiresIn'] as String)
|
|
: (data['expiresIn'] as num?)?.toInt() ?? 3600;
|
|
_tokenExpiry = DateTime.now().add(Duration(seconds: expiresIn - 60));
|
|
}
|
|
|
|
Future<void> _refreshIdToken() async {
|
|
if (_refreshToken == null) {
|
|
return;
|
|
}
|
|
final Uri uri = Uri.parse(
|
|
'https://securetoken.googleapis.com/v1/token'
|
|
'?key=${DefaultFirebaseOptions.web.apiKey}',
|
|
);
|
|
final http.Response response = await http.post(
|
|
uri,
|
|
headers: const <String, String>{
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
},
|
|
body: 'grant_type=refresh_token&refresh_token=$_refreshToken',
|
|
);
|
|
if (response.statusCode != 200) {
|
|
_clearTokens();
|
|
return;
|
|
}
|
|
final Map<String, dynamic> data =
|
|
jsonDecode(response.body) as Map<String, dynamic>;
|
|
_idToken = data['id_token'] as String?;
|
|
_refreshToken = data['refresh_token'] as String? ?? _refreshToken;
|
|
final int expiresIn = (data['expires_in'] as num?)?.toInt() ?? 3600;
|
|
_tokenExpiry = DateTime.now().add(Duration(seconds: expiresIn - 60));
|
|
}
|
|
|
|
void _validateOAuthConfig() {
|
|
if (!_hasOAuthConfig) {
|
|
throw StateError(
|
|
'Linux desktop auth requires googleWebOAuthClientId and '
|
|
'googleOAuthDesktopClientSecret in lib/config/auth_config.dart. '
|
|
'Create a Web application OAuth client in Google Cloud Console '
|
|
'(same project as Firebase) and add '
|
|
'http://127.0.0.1 as an authorized redirect URI.',
|
|
);
|
|
}
|
|
}
|
|
|
|
Future<AppUser> _signInToFirebaseWithIdp(String idToken) async {
|
|
final Uri uri = Uri.parse(
|
|
'https://identitytoolkit.googleapis.com/v1/accounts:signInWithIdp'
|
|
'?key=${DefaultFirebaseOptions.web.apiKey}',
|
|
);
|
|
|
|
final http.Response response = await http.post(
|
|
uri,
|
|
headers: const <String, String>{'Content-Type': 'application/json'},
|
|
body: jsonEncode(<String, dynamic>{
|
|
'postBody': 'id_token=$idToken&providerId=google.com',
|
|
'requestUri': 'http://localhost',
|
|
'returnIdpCredential': true,
|
|
'returnSecureToken': true,
|
|
}),
|
|
);
|
|
|
|
if (response.statusCode != 200) {
|
|
throw StateError(
|
|
'Firebase sign-in failed (${response.statusCode}): ${response.body}',
|
|
);
|
|
}
|
|
|
|
final Map<String, dynamic> data =
|
|
jsonDecode(response.body) as Map<String, dynamic>;
|
|
|
|
_storeTokens(data);
|
|
|
|
return AppUser(
|
|
uid: data['localId'] as String,
|
|
email: data['email'] as String?,
|
|
displayName: data['displayName'] as String?,
|
|
photoUrl: data['photoUrl'] as String?,
|
|
);
|
|
}
|
|
}
|