Skip to content

Commit e70f880

Browse files
committed
login_page: apple auth
1 parent 461d586 commit e70f880

File tree

2 files changed

+54
-4
lines changed

2 files changed

+54
-4
lines changed

lib/api/model/web_auth.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,13 +68,22 @@ class WebAuthPayload {
6868
}
6969
}
7070

71+
/// Whether a URL is hosted by the same org that publishes the app.
72+
bool isAppOwnDomain(Uri url) {
73+
const List<String> appOwnDomains = ['zulip.com', 'zulipchat.com', 'chat.zulip.org'];
74+
return appOwnDomains.any((domain) =>
75+
url.host == domain || url.host.endsWith('.$domain'));
76+
}
77+
7178
String generateOtp() {
7279
final rand = Random.secure();
7380
final Uint8List bytes = Uint8List.fromList(
7481
List.generate(32, (_) => rand.nextInt(256)));
7582
return hex.encode(bytes);
7683
}
7784

85+
String generateRandomToken() => generateOtp();
86+
7887
/// For tests, create an OTP-encrypted API key.
7988
@visibleForTesting
8089
String debugEncodeApiKey(String apiKey, String otp) {

lib/widgets/login.dart

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'dart:async';
33
import 'package:flutter/foundation.dart';
44
import 'package:flutter/material.dart';
55
import 'package:flutter/services.dart';
6+
import 'package:sign_in_with_apple/sign_in_with_apple.dart';
67
import 'package:url_launcher/url_launcher.dart';
78

89
import '../api/exception.dart';
@@ -354,12 +355,10 @@ class _LoginPageState extends State<LoginPage> {
354355
&& defaultTargetPlatform == TargetPlatform.iOS
355356
&& e.message != null && e.message!.startsWith('Error while launching')) {
356357
// Ignore; I've seen this on my iPhone even when auth succeeds.
357-
// Specifically, Apple web auth…which on iOS should be replaced by
358-
// Apple native auth; that's #462.
358+
// Specifically, Apple web auth.
359359
// Possibly related:
360360
// https://github.com/flutter/flutter/issues/91660
361361
// but in that issue, people report authentication not succeeding.
362-
// TODO(#462) remove this?
363362
return;
364363
}
365364

@@ -428,6 +427,48 @@ class _LoginPageState extends State<LoginPage> {
428427
}
429428
}
430429

430+
Future<void> _handleNativeAppleAuth() async {
431+
final state = generateRandomToken();
432+
final credential = await SignInWithApple.getAppleIDCredential(
433+
state: state,
434+
scopes: [
435+
AppleIDAuthorizationScopes.fullName,
436+
AppleIDAuthorizationScopes.email,
437+
],
438+
);
439+
if (credential.state != state) throw Exception('`state` mismatch');
440+
441+
__otp = generateOtp();
442+
443+
final url = widget.serverSettings.realmUrl.resolve('/complete/apple/')
444+
.replace(queryParameters: {'mobile_flow_otp': _otp!, 'native_flow': 'true', 'id_token': credential.identityToken});
445+
446+
await ZulipBinding.instance.launchUrl(url, mode: LaunchMode.inAppBrowserView);
447+
}
448+
449+
bool _canUseNativeAppleFlow() {
450+
// When the platform is iOS, [SignInWithApple.isAvailable] is always true
451+
// because the minimum OS version is 14.0.
452+
if (defaultTargetPlatform != TargetPlatform.iOS) return false;
453+
454+
// The native flow for Apple auth assumes that the app and the server
455+
// are operated by the same organization, so that for a user to
456+
// entrust private information to either one is the same as entrusting
457+
// it to the other. Check that this realm is on such a server.
458+
//
459+
// (For other realms, we'll simply fall back to the web flow, which
460+
// handles things appropriately without relying on that assumption.)
461+
return isAppOwnDomain(widget.serverSettings.realmUrl);
462+
}
463+
464+
Future<void> _handleAuth(ExternalAuthenticationMethod method) async {
465+
if (method.name == 'apple' && _canUseNativeAppleFlow()) {
466+
await _handleNativeAppleAuth();
467+
} else {
468+
await _beginWebAuth(method);
469+
}
470+
}
471+
431472
@override
432473
Widget build(BuildContext context) {
433474
assert(!PerAccountStoreWidget.debugExistsOf(context));
@@ -450,7 +491,7 @@ class _LoginPageState extends State<LoginPage> {
450491
? Image.network(icon, width: 24, height: 24)
451492
: null,
452493
onPressed: !_inProgress
453-
? () => _beginWebAuth(method)
494+
? () => _handleAuth(method)
454495
: null,
455496
label: Text(
456497
zulipLocalizations.signInWithFoo(method.displayName)));

0 commit comments

Comments
 (0)