Skip to content

Commit a698312

Browse files
committed
feat: handle rejection when login via auth code
1 parent 2de90e0 commit a698312

18 files changed

+518
-145
lines changed

l10n/app_en.arb

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,5 +247,21 @@
247247
"@minecraftOwnershipRequiredError": {
248248
"description": "Shown when the Microsoft account is valid and a Minecraft profile may exist, but no active license for Minecraft is found. This may occur if the game has not been purchased or redeemed. Users can resolve this at https://www.minecraft.net/redeem or https://www.minecraft.net/store/minecraft-deluxe-collection-pc."
249249
},
250-
"loginDeviceCodeRejected": "The login attempt was rejected."
250+
"loginAttemptRejected": "The login attempt was rejected.",
251+
"authCodeLoginUnknownError": "An unknown error occurred while logging in: {errorCode}, {errorDescription}",
252+
"@authCodeLoginUnknownError": {
253+
"description": "Shown when an unknown error occurs while logging in with Microsoft via auth code. The user login using in the browser and then Microsoft redirects the user to a minimal and local HTTP server to handles the result. This message is used when the error code is unknown.",
254+
"placeholders": {
255+
"errorCode": {
256+
"type": "String",
257+
"example": "access_denied",
258+
"description": "The error code returned from the API during Microsoft sign-in failure."
259+
},
260+
"errorDescription": {
261+
"type": "String",
262+
"example": "Internal server error.",
263+
"description": "The message returned from the API describing the error."
264+
}
265+
}
266+
}
251267
}

l10n/untranslated.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
"createXboxAccount",
1212
"minecraftAccountNotFoundError",
1313
"minecraftOwnershipRequiredError",
14-
"loginDeviceCodeRejected"
14+
"loginAttemptRejected",
15+
"authCodeLoginUnknownError"
1516
],
1617

1718
"de": [
@@ -26,7 +27,8 @@
2627
"createXboxAccount",
2728
"minecraftAccountNotFoundError",
2829
"minecraftOwnershipRequiredError",
29-
"loginDeviceCodeRejected"
30+
"loginAttemptRejected",
31+
"authCodeLoginUnknownError"
3032
],
3133

3234
"zh": [
@@ -41,6 +43,7 @@
4143
"createXboxAccount",
4244
"minecraftAccountNotFoundError",
4345
"minecraftOwnershipRequiredError",
44-
"loginDeviceCodeRejected"
46+
"loginAttemptRejected",
47+
"authCodeLoginUnknownError"
4548
]
4649
}

lib/account/data/minecraft_account.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,8 @@ class MicrosoftAccountInfo {
140140
),
141141
);
142142

143+
// TODO: Store more data related to Xbox and Microsoft just in case even if it's not needed?
144+
143145
final ExpirableToken microsoftOAuthAccessToken;
144146

145147
// It's unknown when the OAuth refresh token expires. See https://learn.microsoft.com/en-us/entra/identity-platform/refresh-tokens#token-lifetime

lib/account/data/minecraft_accounts.dart

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,7 @@ import 'minecraft_account.dart';
66

77
@immutable
88
class MinecraftAccounts {
9-
const MinecraftAccounts({
10-
required this.all,
11-
// TODO: Use id for both selected and default account instead of index?
12-
required this.defaultAccountId,
13-
});
9+
const MinecraftAccounts({required this.all, required this.defaultAccountId});
1410

1511
factory MinecraftAccounts.empty() =>
1612
const MinecraftAccounts(all: [], defaultAccountId: null);

lib/account/data/minecraft_api/minecraft_api.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ class MinecraftProfileSkin {
8787
);
8888

8989
final String id;
90+
// TODO: Store this as enum since, it's duplicated in MinecraftProfileCape, values are either ACTIVE or INACTIVE
91+
// and do the same in MinecraftAccount
9092
final String state;
9193
final String url;
9294
final String textureKey;
@@ -128,8 +130,6 @@ abstract class MinecraftApi {
128130

129131
Future<bool> checkMinecraftJavaOwnership(String minecraftAccessToken);
130132

131-
// TODO: Handle the case where user don't have Microsoft account, account_creation_required will be thrown when calling: "https://xsts.auth.xboxlive.com/xsts/authorize", cover all cases
132-
// TODO: Create exception for an invalid skin file which is possible. Also enforce file size (accept only small image files)
133133
Future<MinecraftProfileResponse> uploadSkin(
134134
File skinFile, {
135135
required MinecraftSkinVariant skinVariant,

lib/account/logic/account_manager/minecraft_account_manager.dart

Lines changed: 83 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ class MinecraftAccountManager {
107107
Future<AccountResult?> loginWithMicrosoftAuthCode({
108108
required OnAuthProgressUpdateAuthCodeCallback onProgressUpdate,
109109
// The page content is not hardcoded for localization.
110-
required AuthCodeSuccessLoginPageContent successLoginPageContent,
110+
required MicrosoftAuthCodeResponsePageVariants authCodeResponsePageVariants,
111111
}) => _transformExceptions(() async {
112112
final server = httpServer ?? await startServer();
113113

@@ -131,19 +131,66 @@ class MinecraftAccountManager {
131131
return null;
132132
}
133133

134+
Future<void> respondAndStopServer(String html) async {
135+
request.response
136+
..statusCode = HttpStatus.ok
137+
..headers.contentType = ContentType.html
138+
..write(html);
139+
await request.response.close();
140+
await stopServer();
141+
}
142+
134143
final code =
135144
request.uri.queryParameters[MicrosoftConstants
136-
.loginRedirectCodeQueryParamName];
145+
.loginRedirectAuthCodeQueryParamName];
146+
147+
final error =
148+
request.uri.queryParameters[MicrosoftConstants
149+
.loginRedirectErrorQueryParamName];
150+
final errorDescription =
151+
request.uri.queryParameters[MicrosoftConstants
152+
.loginRedirectErrorDescriptionQueryParamName];
153+
154+
if (error != null) {
155+
if (error == MicrosoftConstants.loginRedirectAccessDeniedErrorCode) {
156+
await respondAndStopServer(
157+
buildAuthCodeResultHtmlPage(
158+
authCodeResponsePageVariants.accessDenied,
159+
isSuccess: false,
160+
),
161+
);
162+
throw AccountManagerException.microsoftAuthCodeDenied();
163+
}
164+
165+
await respondAndStopServer(
166+
buildAuthCodeResultHtmlPage(
167+
authCodeResponsePageVariants.unknownError(
168+
error,
169+
errorDescription.toString(),
170+
),
171+
isSuccess: false,
172+
),
173+
);
174+
throw AccountManagerException.microsoftAuthCodeRedirect(
175+
error: error,
176+
errorDescription: errorDescription.toString(),
177+
);
178+
}
137179
if (code == null) {
138-
await stopServer();
139-
throw AccountManagerException.missingAuthCode();
180+
await respondAndStopServer(
181+
buildAuthCodeResultHtmlPage(
182+
authCodeResponsePageVariants.missingAuthCode,
183+
isSuccess: false,
184+
),
185+
);
186+
throw AccountManagerException.microsoftMissingAuthCode();
140187
}
141-
request.response
142-
..statusCode = HttpStatus.ok
143-
..headers.contentType = ContentType.html
144-
..write(buildAuthCodeSuccessPageHtml(successLoginPageContent));
145-
await request.response.close();
146-
await stopServer();
188+
await respondAndStopServer(
189+
buildAuthCodeResultHtmlPage(
190+
authCodeResponsePageVariants.approved,
191+
isSuccess: true,
192+
),
193+
);
147194

148195
onProgressUpdate(MicrosoftAuthProgress.exchangingAuthCode);
149196

@@ -518,8 +565,8 @@ class MinecraftAccountManager {
518565
}
519566

520567
@immutable
521-
class AuthCodeSuccessLoginPageContent {
522-
const AuthCodeSuccessLoginPageContent({
568+
class MicrosoftAuthCodeResponsePageContent {
569+
const MicrosoftAuthCodeResponsePageContent({
523570
required this.pageTitle,
524571
required this.title,
525572
required this.subtitle,
@@ -533,12 +580,33 @@ class AuthCodeSuccessLoginPageContent {
533580
final String pageDir;
534581
}
535582

583+
@immutable
584+
class MicrosoftAuthCodeResponsePageVariants {
585+
const MicrosoftAuthCodeResponsePageVariants({
586+
required this.approved,
587+
required this.accessDenied,
588+
required this.missingAuthCode,
589+
required this.unknownError,
590+
});
591+
592+
final MicrosoftAuthCodeResponsePageContent approved;
593+
final MicrosoftAuthCodeResponsePageContent accessDenied;
594+
final MicrosoftAuthCodeResponsePageContent missingAuthCode;
595+
final MicrosoftAuthCodeResponsePageContent Function(
596+
String errorCode,
597+
String errorDescription,
598+
)
599+
unknownError;
600+
}
601+
536602
// Since the authorization code flow requires a redirect URI,
537603
// the app temporarily starts a local server to handle the redirect request,
538604
// which contains the authorization code needed to complete login.
539605
@visibleForTesting
540-
String buildAuthCodeSuccessPageHtml(AuthCodeSuccessLoginPageContent content) =>
541-
'''
606+
String buildAuthCodeResultHtmlPage(
607+
MicrosoftAuthCodeResponsePageContent content, {
608+
required bool isSuccess,
609+
}) => '''
542610
<!DOCTYPE html>
543611
<html lang="${content.pageLangCode}" dir="${content.pageDir}">
544612
<head>
@@ -575,7 +643,7 @@ String buildAuthCodeSuccessPageHtml(AuthCodeSuccessLoginPageContent content) =>
575643
</head>
576644
<body>
577645
<div class="box">
578-
<h1> ${content.title}</h1>
646+
<h1>${isSuccess ? '✅' : '❌'} ${content.title}</h1>
579647
<p>${content.subtitle}</p>
580648
</div>
581649
</body>

lib/account/logic/account_manager/minecraft_account_manager_exceptions.dart

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,19 @@ import '../../data/minecraft_api/minecraft_api_exceptions.dart';
77
sealed class AccountManagerException implements Exception {
88
const AccountManagerException(this.message);
99

10-
factory AccountManagerException.missingAuthCode() =>
11-
const MissingAuthCodeAccountManagerException();
10+
factory AccountManagerException.microsoftMissingAuthCode() =>
11+
const MicrosoftMissingAuthCodeAccountManagerException();
12+
13+
factory AccountManagerException.microsoftAuthCodeRedirect({
14+
required String error,
15+
required String errorDescription,
16+
}) => MicrosoftAuthCodeRedirectAccountManagerException(
17+
error: error,
18+
errorDescription: errorDescription,
19+
);
20+
21+
factory AccountManagerException.microsoftAuthCodeDenied() =>
22+
const MicrosoftAuthCodeDeniedAccountManagerException();
1223

1324
factory AccountManagerException.microsoftAuthApiException(
1425
MicrosoftAuthException authApiException,
@@ -29,11 +40,32 @@ sealed class AccountManagerException implements Exception {
2940
String toString() => message;
3041
}
3142

32-
final class MissingAuthCodeAccountManagerException
43+
final class MicrosoftMissingAuthCodeAccountManagerException
44+
extends AccountManagerException {
45+
const MicrosoftMissingAuthCodeAccountManagerException()
46+
: super(
47+
'The Microsoft auth code query parameter should be passed to the redirect URL but was not found.',
48+
);
49+
}
50+
51+
final class MicrosoftAuthCodeRedirectAccountManagerException
52+
extends AccountManagerException {
53+
const MicrosoftAuthCodeRedirectAccountManagerException({
54+
required this.error,
55+
required this.errorDescription,
56+
}) : super(
57+
'While logging via auth code, Microsoft redirected the result which is an unknown error: "$error", description: "$errorDescription".',
58+
);
59+
60+
final String error;
61+
final String errorDescription;
62+
}
63+
64+
final class MicrosoftAuthCodeDeniedAccountManagerException
3365
extends AccountManagerException {
34-
const MissingAuthCodeAccountManagerException()
66+
const MicrosoftAuthCodeDeniedAccountManagerException()
3567
: super(
36-
'The auth code query parameter should be passed to the redirect URL but was not found.',
68+
'While logging with Microsoft via auth code, the user has denied the authorization request.',
3769
);
3870
}
3971

lib/account/logic/microsoft/cubit/microsoft_account_handler_cubit.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ class MicrosoftAccountHandlerCubit extends Cubit<MicrosoftAccountHandlerState> {
4343
}
4444

4545
Future<void> loginWithMicrosoftAuthCode({
46-
required AuthCodeSuccessLoginPageContent successLoginPageContent,
46+
// The page content is not hardcoded for localization.
47+
required MicrosoftAuthCodeResponsePageVariants authCodeResponsePageVariants,
4748
}) => _handleErrors(() async {
4849
await minecraftAccountManager.startServer();
4950
final result = await minecraftAccountManager.loginWithMicrosoftAuthCode(
@@ -55,7 +56,7 @@ class MicrosoftAccountHandlerCubit extends Cubit<MicrosoftAccountHandlerState> {
5556
authCodeLoginUrl: authCodeLoginUrl,
5657
),
5758
),
58-
successLoginPageContent: successLoginPageContent,
59+
authCodeResponsePageVariants: authCodeResponsePageVariants,
5960
);
6061
if (result == null) {
6162
// The user closed the login dialog without completing the login.

lib/account/ui/login_with_microsoft_dialog.dart

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -162,10 +162,20 @@ class _LoginWithMicrosoftDialogState extends State<LoginWithMicrosoftDialog> {
162162
children: [
163163
const SizedBox(height: 6),
164164
FilledButton.icon(
165-
onPressed:
166-
() => _microsoftAccountHandlerCubit.loginWithMicrosoftAuthCode(
167-
successLoginPageContent:
168-
AuthCodeSuccessLoginPageContent(
165+
onPressed: () {
166+
final pageDir = Directionality.of(context).name;
167+
final pageLangCode =
168+
context
169+
.read<SettingsCubit>()
170+
.state
171+
.settings
172+
.general
173+
.appLanguage
174+
.localeCode;
175+
_microsoftAccountHandlerCubit.loginWithMicrosoftAuthCode(
176+
authCodeResponsePageVariants:
177+
MicrosoftAuthCodeResponsePageVariants(
178+
approved: MicrosoftAuthCodeResponsePageContent(
169179
pageTitle:
170180
context
171181
.loc
@@ -178,17 +188,41 @@ class _LoginWithMicrosoftDialogState extends State<LoginWithMicrosoftDialog> {
178188
.authCodeRedirectPageLoginSuccessMessage(
179189
ProjectInfoConstants.displayName,
180190
),
181-
pageDir: Directionality.of(context).name,
182-
pageLangCode:
183-
context
184-
.read<SettingsCubit>()
185-
.state
186-
.settings
187-
.general
188-
.appLanguage
189-
.localeCode,
191+
pageDir: pageDir,
192+
pageLangCode: pageLangCode,
190193
),
191-
),
194+
accessDenied:
195+
MicrosoftAuthCodeResponsePageContent(
196+
pageTitle: context.loc.errorOccurred,
197+
title: context.loc.errorOccurred,
198+
subtitle: context.loc.loginAttemptRejected,
199+
pageLangCode: pageLangCode,
200+
pageDir: pageDir,
201+
),
202+
missingAuthCode:
203+
MicrosoftAuthCodeResponsePageContent(
204+
pageTitle: context.loc.errorOccurred,
205+
title: context.loc.errorOccurred,
206+
subtitle: context.loc.missingAuthCodeError,
207+
pageLangCode: pageLangCode,
208+
pageDir: pageDir,
209+
),
210+
unknownError:
211+
(errorCode, errorDescription) =>
212+
MicrosoftAuthCodeResponsePageContent(
213+
pageTitle: context.loc.errorOccurred,
214+
title: context.loc.errorOccurred,
215+
subtitle: context.loc
216+
.authCodeLoginUnknownError(
217+
errorCode,
218+
errorDescription,
219+
),
220+
pageLangCode: pageLangCode,
221+
pageDir: pageDir,
222+
),
223+
),
224+
);
225+
},
192226
label: Text(context.loc.signInWithMicrosoft),
193227
icon: const Icon(Icons.open_in_browser),
194228
),
@@ -272,7 +306,7 @@ class _LoginWithMicrosoftDialogState extends State<LoginWithMicrosoftDialog> {
272306
spacing: 8,
273307
children: [
274308
Text(
275-
context.loc.loginDeviceCodeRejected,
309+
context.loc.loginAttemptRejected,
276310
style: context.theme.textTheme.titleMedium,
277311
),
278312
ElevatedButton.icon(

0 commit comments

Comments
 (0)