-
Notifications
You must be signed in to change notification settings - Fork 309
login: Support web-based auth methods #600
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
fb8bb4e
0795586
70233ef
a88135a
a2ed21b
84ff624
aa8b91b
a8d4888
e4cf955
d6d3199
1bd1fd7
60c5245
bd2fc3d
76ba652
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
import 'dart:math'; | ||
|
||
import 'package:convert/convert.dart'; | ||
import 'package:flutter/foundation.dart'; | ||
|
||
/// The authentication information contained in the zulip:// redirect URL. | ||
class WebAuthPayload { | ||
final Uri realm; | ||
final String email; | ||
final int? userId; // TODO(server-5) new in FL 108 | ||
final String otpEncryptedApiKey; | ||
|
||
WebAuthPayload._({ | ||
required this.realm, | ||
required this.email, | ||
required this.userId, | ||
required this.otpEncryptedApiKey, | ||
}); | ||
|
||
factory WebAuthPayload.parse(Uri url) { | ||
if ( | ||
url case Uri( | ||
scheme: 'zulip', | ||
host: 'login', | ||
queryParameters: { | ||
'realm': String realmStr, | ||
'email': String email, | ||
// 'user_id' handled below | ||
'otp_encrypted_api_key': String otpEncryptedApiKey, | ||
}, | ||
) | ||
) { | ||
final Uri? realm = Uri.tryParse(realmStr); | ||
if (realm == null) throw const FormatException(); | ||
|
||
// TODO(server-5) require in queryParameters (new in FL 108) | ||
final userIdStr = url.queryParameters['user_id']; | ||
int? userId; | ||
if (userIdStr != null) { | ||
userId = int.tryParse(userIdStr, radix: 10); | ||
if (userId == null) throw const FormatException(); | ||
} | ||
|
||
if (!RegExp(r'^[0-9a-fA-F]{64}$').hasMatch(otpEncryptedApiKey)) { | ||
throw const FormatException(); | ||
} | ||
|
||
return WebAuthPayload._( | ||
otpEncryptedApiKey: otpEncryptedApiKey, | ||
email: email, | ||
userId: userId, | ||
realm: realm, | ||
); | ||
} else { | ||
// TODO(dart): simplify after https://github.com/dart-lang/language/issues/2537 | ||
throw const FormatException(); | ||
} | ||
} | ||
|
||
String decodeApiKey(String otp) { | ||
final otpBytes = hex.decode(otp); | ||
final otpEncryptedApiKeyBytes = hex.decode(otpEncryptedApiKey); | ||
if (otpBytes.length != otpEncryptedApiKeyBytes.length) { | ||
throw const FormatException(); | ||
} | ||
return String.fromCharCodes(Iterable.generate(otpBytes.length, | ||
(i) => otpBytes[i] ^ otpEncryptedApiKeyBytes[i])); | ||
} | ||
} | ||
|
||
String generateOtp() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's give this a unit test, too. For a smoke test: it returns a value (rather than throw), and the value is a hex string of the right length. And then given that a previous draft of this function elsewhere actually had a bug where the randomness was off — it had We can follow the lead of our BackoffMachine test and say the probability of a false failure just needs to be under 1e-9. So…
That should be plenty quick to run, I hope. And each possible byte value gets N * 32 opportunities to show up, each with probability 1/256; so its probability of missing all of those is exp(- N * 32 / 256) < 2e-12, and there are 256 such possible byte values so the probability that any of them gets missed is < 1e-9. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Then since we'll be generating all those sample results, may as well use those to subsume the smoke test: just check each of them has the right length (and we'll already be decoding them as hex so implicitly checking they're hex strings). One possible failure mode of a buggy implementation could be to drop leading zero bytes, for example, and this check would catch that. |
||
final rand = Random.secure(); | ||
final Uint8List bytes = Uint8List.fromList( | ||
List.generate(32, (_) => rand.nextInt(256))); | ||
return hex.encode(bytes); | ||
} | ||
|
||
/// For tests, create an OTP-encrypted API key. | ||
@visibleForTesting | ||
String debugEncodeApiKey(String apiKey, String otp) { | ||
final apiKeyBytes = apiKey.codeUnits; | ||
assert(apiKeyBytes.every((byte) => byte <= 0xff)); | ||
final otpBytes = hex.decode(otp); | ||
assert(apiKeyBytes.length == otpBytes.length); | ||
return hex.encode(List.generate(otpBytes.length, | ||
(i) => apiKeyBytes[i] ^ otpBytes[i])); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, the changes here and in Info.plist LGTM.
(The intent filter and
CFBundleUrlTypes
match what we have in zulip-mobile; the other bits are what's prescribed in the docs under https://docs.flutter.dev/ui/navigation/deep-linking .)So what remains to do on this PR is those small comments at #600 (review) on the new test, and then the bigger point from my end-to-end testing after that.