feat: Implement Twitter (X) login using OAuth 2.0 PKCE flow via webview.
This commit is contained in:
107
lib/core/services/twitter_auth_service.dart
Normal file
107
lib/core/services/twitter_auth_service.dart
Normal file
@@ -0,0 +1,107 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
/// Twitter OAuth 2.0 PKCE flow for mobile.
|
||||
class TwitterAuthService {
|
||||
static const _clientId = 'eWEzaDJ0al9lX1diOGY0ejRYeXM6MTpjaQ';
|
||||
// Must match the callback URL configured in Twitter Developer Portal
|
||||
static const _redirectUri = 'https://dev.re-quest.com/auth/callback/twitter';
|
||||
static const _scope = 'tweet.read users.read offline.access';
|
||||
|
||||
late final String _codeVerifier;
|
||||
late final String _codeChallenge;
|
||||
late final String _stateParam;
|
||||
|
||||
TwitterAuthService() {
|
||||
_codeVerifier = _generateCodeVerifier();
|
||||
_codeChallenge = _generateCodeChallenge(_codeVerifier);
|
||||
_stateParam = _generateRandomString(32);
|
||||
}
|
||||
|
||||
/// Build the Twitter authorization URL to open in a webview.
|
||||
String get authorizationUrl {
|
||||
final params = {
|
||||
'response_type': 'code',
|
||||
'client_id': _clientId,
|
||||
'redirect_uri': _redirectUri,
|
||||
'scope': _scope,
|
||||
'state': _stateParam,
|
||||
'code_challenge': _codeChallenge,
|
||||
'code_challenge_method': 'S256',
|
||||
};
|
||||
final query = params.entries
|
||||
.map((e) => '${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value)}')
|
||||
.join('&');
|
||||
return 'https://twitter.com/i/oauth2/authorize?$query';
|
||||
}
|
||||
|
||||
/// Check if a URL is the redirect callback and extract the auth code.
|
||||
String? extractCodeFromUrl(String url) {
|
||||
if (!url.startsWith(_redirectUri)) return null;
|
||||
final uri = Uri.parse(url);
|
||||
final state = uri.queryParameters['state'];
|
||||
if (state != _stateParam) return null;
|
||||
return uri.queryParameters['code'];
|
||||
}
|
||||
|
||||
/// Exchange the auth code for tokens and fetch user info.
|
||||
Future<Map<String, dynamic>?> exchangeCodeForUser(String code) async {
|
||||
final dio = Dio();
|
||||
|
||||
// Exchange code for access token
|
||||
final tokenResponse = await dio.post(
|
||||
'https://api.twitter.com/2/oauth2/token',
|
||||
options: Options(
|
||||
contentType: 'application/x-www-form-urlencoded',
|
||||
headers: {
|
||||
'Authorization': 'Basic ${base64Encode(utf8.encode('$_clientId:'))}',
|
||||
},
|
||||
),
|
||||
data: {
|
||||
'code': code,
|
||||
'grant_type': 'authorization_code',
|
||||
'redirect_uri': _redirectUri,
|
||||
'code_verifier': _codeVerifier,
|
||||
},
|
||||
);
|
||||
|
||||
final accessToken = tokenResponse.data['access_token'] as String?;
|
||||
if (accessToken == null) return null;
|
||||
|
||||
// Fetch user info
|
||||
final userResponse = await dio.get(
|
||||
'https://api.twitter.com/2/users/me',
|
||||
queryParameters: {'user.fields': 'id,name,username,profile_image_url'},
|
||||
options: Options(
|
||||
headers: {'Authorization': 'Bearer $accessToken'},
|
||||
),
|
||||
);
|
||||
|
||||
final userData = userResponse.data['data'] as Map<String, dynamic>?;
|
||||
if (userData == null) return null;
|
||||
|
||||
return {
|
||||
'id': userData['id'],
|
||||
'name': userData['name'],
|
||||
'username': userData['username'],
|
||||
'avatar': userData['profile_image_url'],
|
||||
};
|
||||
}
|
||||
|
||||
String _generateRandomString(int length) {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
|
||||
final random = Random.secure();
|
||||
return List.generate(length, (_) => chars[random.nextInt(chars.length)]).join();
|
||||
}
|
||||
|
||||
String _generateCodeVerifier() => _generateRandomString(64);
|
||||
|
||||
String _generateCodeChallenge(String verifier) {
|
||||
final bytes = utf8.encode(verifier);
|
||||
final digest = sha256.convert(bytes);
|
||||
return base64Url.encode(digest.bytes).replaceAll('=', '');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user