Skip to content

Commit

Permalink
Implement issue #193 Cannot add 2FA Protected account (or feature #195
Browse files Browse the repository at this point in the history
…Two factor authentication support).
  • Loading branch information
j-fbriere committed Feb 9, 2024
1 parent c98e0dd commit 735ad99
Show file tree
Hide file tree
Showing 10 changed files with 164 additions and 50 deletions.
13 changes: 11 additions & 2 deletions lib/client/client_account.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_triple/flutter_triple.dart';
import 'package:http/http.dart' as http;
import 'package:logging/logging.dart';
Expand All @@ -21,6 +22,14 @@ class TwitterAccount {
static final Map<String,List<Map<String,int>>> _rateLimits = {};
static final List<TwitterTokenEntity> _twitterTokenToDeleteLst = [];

static BuildContext? _currentContext;
static String? _currentLanguageCode;

static void setCurrentContext(BuildContext currentContext) {
_currentContext = currentContext;
_currentLanguageCode = Localizations.localeOf(currentContext).languageCode;
}

static int nbrGuestAccounts() {
return _twitterTokenLst.where((e) => e.guest).length;
}
Expand Down Expand Up @@ -169,7 +178,7 @@ class TwitterAccount {
TwitterTokenEntity? tte = _twitterTokenLst.firstWhereOrNull((e) => e.profile != null && e.profile!.username == tpe.username);
if (tte != null) {
if (DateTime.now().difference(tte.createdAt).inDays >= 30) {
TwitterTokenEntity newTte = await TwitterRegularAccount.createRegularTwitterToken(tpe.username, tpe.password, tpe.name, tpe.email, tpe.phone);
TwitterTokenEntity newTte = await TwitterRegularAccount.createRegularTwitterToken(_currentContext, _currentLanguageCode, tpe.username, tpe.password, tpe.name, tpe.email, tpe.phone);
addTwitterToken(newTte);
markTwitterTokenForDeletion(tte);
}
Expand Down Expand Up @@ -360,7 +369,7 @@ class TwitterAccount {

static Future<TwitterTokenEntity> createRegularTwitterToken(String username, String password, String? name, String? email, String? phone) async {
TwitterTokenEntity? oldTte = _twitterTokenLst.firstWhereOrNull((e) => !e.guest && e.screenName == username);
TwitterTokenEntity newTte = await TwitterRegularAccount.createRegularTwitterToken(username, password, name, email, phone);
TwitterTokenEntity newTte = await TwitterRegularAccount.createRegularTwitterToken(_currentContext, _currentLanguageCode, username, password, name, email, phone);
if (oldTte != null) {
markTwitterTokenForDeletion(oldTte);
await deleteTwitterTokensMarkedForDeletion();
Expand Down
110 changes: 100 additions & 10 deletions lib/client/client_regular_account.dart
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import 'dart:convert';
import 'package:squawker/database/entities.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:logging/logging.dart';
import 'package:squawker/client/client_account.dart';
import 'package:squawker/database/entities.dart';
import 'package:squawker/generated/l10n.dart';

class TwitterRegularAccount {
static final log = Logger('TwitterRegularAccount');

static Future<String> _getLoginFlowToken(Map<String,String> headers, String accessToken, String guestToken) async {
log.info('Posting https://api.twitter.com/1.1/onboarding/task.json?flow_name=login&api_version=1&known_device_token=&sim_country_code=us');
log.info('Posting https://api.twitter.com/1.1/onboarding/task.json?flow_name=login');
headers.addAll({
'Authorization': 'Bearer $accessToken',
'X-Guest-Token': guestToken
});
var response = await http.post(Uri.parse('https://api.twitter.com/1.1/onboarding/task.json?flow_name=login&api_version=1&known_device_token=&sim_country_code=us'),
var response = await http.post(Uri.parse('https://api.twitter.com/1.1/onboarding/task.json?flow_name=login'),
headers: headers,
body: json.encode({
'flow_token': null,
Expand Down Expand Up @@ -107,9 +109,13 @@ class TwitterRegularAccount {
throw TwitterAccountException('Unable to get the password flow token. The response (${response.statusCode}) from Twitter/X was: ${response.body}');
}

static Future<Map<String,dynamic>> _getDuplicationCheckFlowToken(Map<String,String> headers, String flowToken) async {
log.info('Posting (duplication check) https://api.twitter.com/1.1/onboarding/task.json');
var response = await http.post(Uri.parse('https://api.twitter.com/1.1/onboarding/task.json'),
static Future<Map<String,dynamic>> _getDuplicationCheckFlowToken(Map<String,String> headers, String flowToken, String? languageCode) async {
String url = 'https://api.twitter.com/1.1/onboarding/task.json';
if (languageCode != null) {
url = '$url?lang=$languageCode';
}
log.info('Posting (duplication check) $url');
var response = await http.post(Uri.parse(url),
headers: headers,
body: json.encode({
'flow_token': flowToken,
Expand Down Expand Up @@ -147,16 +153,58 @@ class TwitterRegularAccount {
throw TwitterAccountException('Unable to get the duplication check flow token. The response (${response.statusCode}) from Twitter/X was: ${response.body}');
}

static Future<TwitterTokenEntity> createRegularTwitterToken(String username, String password, String? name, String? email, String? phone) async {
static Future<Map<String,dynamic>> _get2FAFlowToken(Map<String,String> headers, String flowToken, String code) async {
log.info('Posting (2FA) https://api.twitter.com/1.1/onboarding/task.json');
var response = await http.post(Uri.parse('https://api.twitter.com/1.1/onboarding/task.json'),
headers: headers,
body: json.encode({
'flow_token': flowToken,
'subtask_inputs': [
{
'enter_text': {
'text': code,
'link': 'next_link'
},
'subtask_id': 'LoginTwoFactorAuthChallenge'
}
]
})
);

if (response.statusCode == 200) {
var result = jsonDecode(response.body);
return result;
}

throw TwitterAccountException('Unable to get the 2FA flow token. The response (${response.statusCode}) from Twitter/X was: ${response.body}');
}

static Future<TwitterTokenEntity> createRegularTwitterToken(BuildContext? context, String? languageCode, String username, String password, String? name, String? email, String? phone) async {
String accessToken = await TwitterAccount.getAccessToken();
String guestToken = await TwitterAccount.getGuestToken(accessToken);
Map<String,String> headers = TwitterAccount.initHeaders();
String flowToken = await _getLoginFlowToken(headers, accessToken, guestToken);
flowToken = await _getUsernameFlowToken(headers, flowToken, username);
flowToken = await _getPasswordFlowToken(headers, flowToken, password);
Map<String,dynamic> res = await _getDuplicationCheckFlowToken(headers, flowToken);
if (res['subtasks']?[0]?['open_account'] != null) {
Map<String,dynamic> openAccount = res['subtasks'][0]['open_account'] as Map<String,dynamic>;
Map<String,dynamic> res = await _getDuplicationCheckFlowToken(headers, flowToken, languageCode);
Map<String,dynamic>? openAccount;
if (res['subtasks']?[0]?['subtask_id'] == 'LoginTwoFactorAuthChallenge') {
if (context != null) {
flowToken = res['flow_token'];
Map<String,dynamic> head = res['subtasks'][0]['enter_text']['header'];
String? code = await askForTwoFactorCode(context, head['primary_text']['text'] as String, head['secondary_text']['text'] as String);
if (code != null) {
res = await _get2FAFlowToken(headers, flowToken, code);
if (res['subtasks']?[0]?['subtask_id'] == 'LoginSuccessSubtask') {
openAccount = res['subtasks'][0]['open_account'] as Map<String,dynamic>;
}
}
}
}
else if (res['subtasks']?[0]?['subtask_id'] == 'LoginSuccessSubtask') {
openAccount = res['subtasks'][0]['open_account'] as Map<String,dynamic>;
}
if (openAccount != null) {
TwitterTokenEntity tte = TwitterTokenEntity(
guest: false,
idStr: (openAccount['user'] as Map<String,dynamic>)['id_str'] as String,
Expand All @@ -172,5 +220,47 @@ class TwitterRegularAccount {
}
throw TwitterAccountException('Unable to create the regular Twitter/X token. The response from Twitter/X was: $res');
}

static Future<String?> askForTwoFactorCode(BuildContext context, String primaryText, String secondaryText) async {
String? code;
return await showDialog<String?>(
barrierDismissible: false,
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(primaryText),
titleTextStyle: TextStyle(fontSize: Theme.of(context).textTheme.titleMedium!.fontSize, color: Theme.of(context).textTheme.titleMedium!.color, fontWeight: FontWeight.bold),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(secondaryText, style: TextStyle(fontSize: Theme.of(context).textTheme.labelMedium!.fontSize)),
SizedBox(height: 20),
TextField(
decoration: InputDecoration(contentPadding: EdgeInsets.all(5)),
onChanged: (value) async {
code = value;
},
)
]
),
actionsAlignment: MainAxisAlignment.center,
actions: [
ElevatedButton(
child: Text(L10n.current.cancel),
onPressed: () {
Navigator.of(context).pop(null);
},
),
ElevatedButton(
child: Text(L10n.current.ok),
onPressed: () {
Navigator.of(context).pop(code);
},
),
],
);
}
);
}
}

10 changes: 10 additions & 0 deletions lib/generated/intl/messages_zh_Hans.dart
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("数据导入成功"),
"date_created": MessageLookupByLibrary.simpleMessage("创建日期"),
"date_subscribed": MessageLookupByLibrary.simpleMessage("订阅日期"),
"default_subscription_tab":
MessageLookupByLibrary.simpleMessage("关注页默认标签"),
"default_tab": MessageLookupByLibrary.simpleMessage("默认页面"),
"delete": MessageLookupByLibrary.simpleMessage("删除"),
"disable_screenshots": MessageLookupByLibrary.simpleMessage("禁用截屏"),
Expand Down Expand Up @@ -199,11 +201,16 @@ class MessageLookup extends MessageLookupByLibrary {
"include_replies": MessageLookupByLibrary.simpleMessage("包括回复"),
"include_retweets": MessageLookupByLibrary.simpleMessage("包括转推"),
"joined": m8,
"keep_feed_offset_description":
MessageLookupByLibrary.simpleMessage("应用重启时,会保持时间轴的滚动位置不变"),
"keep_feed_offset_label":
MessageLookupByLibrary.simpleMessage("从上次的位置继续浏览时间轴"),
"language": MessageLookupByLibrary.simpleMessage("语言"),
"language_subtitle": MessageLookupByLibrary.simpleMessage("需要重启应用"),
"large": MessageLookupByLibrary.simpleMessage("大"),
"leaner_feeds_description":
MessageLookupByLibrary.simpleMessage("预览链接不显示在来自源的推文中"),
"leaner_feeds_label": MessageLookupByLibrary.simpleMessage("时间轴紧凑模式"),
"legacy_android_import":
MessageLookupByLibrary.simpleMessage("从旧的 Android 设备导入"),
"let_the_developers_know_if_something_is_broken":
Expand Down Expand Up @@ -419,8 +426,11 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("是否隐藏被标记为敏感的推文"),
"which_tab_is_shown_when_the_app_opens":
MessageLookupByLibrary.simpleMessage("打开应用时显示哪个页面"),
"which_tab_is_shown_when_the_subscription_opens":
MessageLookupByLibrary.simpleMessage("打开关注页时展示哪个标签"),
"would_you_like_to_enable_automatic_error_reporting":
MessageLookupByLibrary.simpleMessage("您希望自动发送错误报告吗?"),
"x_api": MessageLookupByLibrary.simpleMessage("X API"),
"yes": MessageLookupByLibrary.simpleMessage("好"),
"yes_please": MessageLookupByLibrary.simpleMessage("是,请让我看"),
"you_have_not_saved_any_tweets_yet":
Expand Down
1 change: 1 addition & 0 deletions lib/group/_feed.dart
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,7 @@ class SubscriptionGroupFeedState extends State<SubscriptionGroupFeed> with Widge

@override
Widget build(BuildContext context) {
TwitterAccount.setCurrentContext(context);
BasePrefService prefs = PrefService.of(context, listen: false);
_keepFeedOffset = prefs.get(optionKeepFeedOffset);

Expand Down
2 changes: 2 additions & 0 deletions lib/profile/_tweets.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:pref/pref.dart';

import 'package:squawker/client/client.dart';
import 'package:squawker/client/client_account.dart';
import 'package:squawker/constants.dart';
import 'package:squawker/profile/profile.dart';
import 'package:squawker/tweet/conversation.dart';
Expand Down Expand Up @@ -80,6 +81,7 @@ class _ProfileTweetsState extends State<ProfileTweets> with AutomaticKeepAliveCl
@override
Widget build(BuildContext context) {
super.build(context);
TwitterAccount.setCurrentContext(context);

return Consumer<TweetContextState>(builder: (context, model, child) {
if (model.hideSensitive && (widget.user.possiblySensitive ?? false)) {
Expand Down
2 changes: 2 additions & 0 deletions lib/profile/profile.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_triple/flutter_triple.dart';
import 'package:squawker/client/client_account.dart';
import 'package:squawker/constants.dart';
import 'package:squawker/database/entities.dart';
import 'package:squawker/generated/l10n.dart';
Expand Down Expand Up @@ -235,6 +236,7 @@ class _ProfileScreenBodyState extends State<ProfileScreenBody> with TickerProvid

@override
Widget build(BuildContext context) {
TwitterAccount.setCurrentContext(context);
// TODO: This shouldn't happen before the profile is loaded
var user = widget.profile.user;
if (user.idStr == null) {
Expand Down
2 changes: 2 additions & 0 deletions lib/search/search.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_triple/flutter_triple.dart';
import 'package:squawker/client/client.dart';
import 'package:squawker/client/client_account.dart';
import 'package:squawker/constants.dart';
import 'package:squawker/database/entities.dart';
import 'package:squawker/generated/l10n.dart';
Expand Down Expand Up @@ -74,6 +75,7 @@ class _SearchScreenState extends State<_SearchScreen> with SingleTickerProviderS

@override
Widget build(BuildContext context) {
TwitterAccount.setCurrentContext(context);
var subscriptionsModel = context.read<SubscriptionsModel>();

var prefs = PrefService.of(context, listen: false);
Expand Down
Loading

0 comments on commit 735ad99

Please sign in to comment.