Skip to content

Commit

Permalink
feat(shorebird_cli): use Google OAuth 2.0 (#223)
Browse files Browse the repository at this point in the history
  • Loading branch information
felangel authored Apr 4, 2023
1 parent eb4d822 commit efdb675
Show file tree
Hide file tree
Showing 35 changed files with 375 additions and 227 deletions.
105 changes: 81 additions & 24 deletions packages/shorebird_cli/lib/src/auth/auth.dart
Original file line number Diff line number Diff line change
@@ -1,54 +1,111 @@
import 'dart:convert';
import 'dart:io';

import 'package:googleapis_auth/auth_io.dart';
import 'package:http/http.dart' as http;
import 'package:path/path.dart' as p;
import 'package:shorebird_cli/src/auth/session.dart';
import 'package:shorebird_cli/src/config/config.dart';

export 'package:googleapis_auth/googleapis_auth.dart' show AccessCredentials;

final _clientId = ClientId(
/// Shorebird CLI's OAuth 2.0 identifier.
'523302233293-eia5antm0tgvek240t46orctktiabrek.apps.googleusercontent.com',

/// Shorebird CLI's OAuth 2.0 secret.
///
/// This isn't actually meant to be kept secret.
/// There is no way to properly secure a secret for installed/console applications.
/// Fortunately the OAuth2 flow used in this case assumes that the app cannot
/// keep secrets so this particular secret DOES NOT need to be kept secret.
/// You should however make sure not to re-use the same secret
/// anywhere secrecy is required.
///
/// For more info see: https://developers.google.com/identity/protocols/oauth2/native-app
'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',
);
final _scopes = ['openid', 'https://www.googleapis.com/auth/userinfo.email'];

typedef ObtainAccessCredentials = Future<AccessCredentials> Function(
ClientId clientId,
List<String> scopes,
http.Client client,
void Function(String) userPrompt,
);

class Auth {
Auth() {
_loadSession();
Auth({
http.Client? httpClient,
ObtainAccessCredentials? obtainAccessCredentials,
}) : _httpClient = httpClient ?? http.Client(),
_obtainAccessCredentials =
obtainAccessCredentials ?? obtainAccessCredentialsViaUserConsent {
_loadCredentials();
}

static const _sessionFileName = 'shorebird-session.json';
final sessionFilePath = p.join(shorebirdConfigDir, _sessionFileName);
static const _credentialsFileName = 'credentials.json';

final http.Client _httpClient;
final ObtainAccessCredentials _obtainAccessCredentials;
final credentialsFilePath = p.join(shorebirdConfigDir, _credentialsFileName);

void login({required String apiKey}) {
_session = Session(apiKey: apiKey);
_flushSession(_session!);
http.Client get client {
if (credentials == null) return _httpClient;
return autoRefreshingClient(_clientId, credentials!, _httpClient);
}

void logout() => _clearSession();
Future<void> login(void Function(String) prompt) async {
if (credentials != null) return;

Session? _session;
final client = http.Client();
try {
_credentials = await _obtainAccessCredentials(
_clientId,
_scopes,
client,
prompt,
);
_flushCredentials(_credentials!);
} finally {
client.close();
}
}

void logout() => _clearCredentials();

AccessCredentials? _credentials;

Session? get currentSession => _session;
AccessCredentials? get credentials => _credentials;

void _loadSession() {
final sessionFile = File(sessionFilePath);
void _loadCredentials() {
final credentialsFile = File(credentialsFilePath);

if (sessionFile.existsSync()) {
if (credentialsFile.existsSync()) {
try {
final contents = sessionFile.readAsStringSync();
_session = Session.fromJson(
final contents = credentialsFile.readAsStringSync();
_credentials = AccessCredentials.fromJson(
json.decode(contents) as Map<String, dynamic>,
);
} catch (_) {}
}
}

void _flushSession(Session session) {
File(sessionFilePath)
void _flushCredentials(AccessCredentials credentials) {
File(credentialsFilePath)
..createSync(recursive: true)
..writeAsStringSync(json.encode(session.toJson()));
..writeAsStringSync(json.encode(credentials.toJson()));
}

void _clearSession() {
_session = null;
void _clearCredentials() {
_credentials = null;

final sessionFile = File(sessionFilePath);
if (sessionFile.existsSync()) {
sessionFile.deleteSync(recursive: true);
final credentialsFile = File(credentialsFilePath);
if (credentialsFile.existsSync()) {
credentialsFile.deleteSync(recursive: true);
}
}

void close() {
_httpClient.close();
}
}
13 changes: 0 additions & 13 deletions packages/shorebird_cli/lib/src/auth/session.dart

This file was deleted.

3 changes: 2 additions & 1 deletion packages/shorebird_cli/lib/src/command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'dart:io';

import 'package:args/args.dart';
import 'package:args/command_runner.dart';
import 'package:http/http.dart' as http;
import 'package:mason_logger/mason_logger.dart';
import 'package:meta/meta.dart';
import 'package:shorebird_cli/src/auth/auth.dart';
Expand All @@ -12,7 +13,7 @@ import 'package:shorebird_code_push_client/shorebird_code_push_client.dart';
typedef HashFunction = String Function(List<int> bytes);

typedef CodePushClientBuilder = CodePushClient Function({
required String apiKey,
required http.Client httpClient,
Uri? hostedUri,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,7 @@ Defaults to the name in "pubspec.yaml".''',

@override
Future<int>? run() async {
final session = auth.currentSession;
if (session == null) {
if (auth.credentials == null) {
logger.err('You must be logged in.');
return ExitCode.noUser.code;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@ Defaults to the app_id in "shorebird.yaml".''',

@override
Future<int>? run() async {
final session = auth.currentSession;
if (session == null) {
if (auth.credentials == null) {
logger.err('You must be logged in.');
return ExitCode.noUser.code;
}
Expand All @@ -56,7 +55,7 @@ Defaults to the app_id in "shorebird.yaml".''',
}

final client = buildCodePushClient(
apiKey: session.apiKey,
httpClient: auth.client,
hostedUri: hostedUri,
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,13 @@ class ListAppsCommand extends ShorebirdCommand with ShorebirdConfigMixin {

@override
Future<int>? run() async {
final session = auth.currentSession;
if (session == null) {
if (auth.credentials == null) {
logger.err('You must be logged in.');
return ExitCode.noUser.code;
}

final client = buildCodePushClient(
apiKey: session.apiKey,
httpClient: auth.client,
hostedUri: hostedUri,
);

Expand Down
2 changes: 1 addition & 1 deletion packages/shorebird_cli/lib/src/commands/build_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class BuildCommand extends ShorebirdCommand

@override
Future<int> run() async {
if (auth.currentSession == null) {
if (auth.credentials == null) {
logger
..err('You must be logged in to build.')
..err("Run 'shorebird login' to log in and try again.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,13 @@ class CreateChannelsCommand extends ShorebirdCommand with ShorebirdConfigMixin {

@override
Future<int>? run() async {
final session = auth.currentSession;
if (session == null) {
if (auth.credentials == null) {
logger.err('You must be logged in to view channels.');
return ExitCode.noUser.code;
}

final client = buildCodePushClient(
apiKey: session.apiKey,
httpClient: auth.client,
hostedUri: hostedUri,
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,13 @@ class ListChannelsCommand extends ShorebirdCommand with ShorebirdConfigMixin {

@override
Future<int>? run() async {
final session = auth.currentSession;
if (session == null) {
if (auth.credentials == null) {
logger.err('You must be logged in to view channels.');
return ExitCode.noUser.code;
}

final client = buildCodePushClient(
apiKey: session.apiKey,
httpClient: auth.client,
hostedUri: hostedUri,
);

Expand Down
5 changes: 2 additions & 3 deletions packages/shorebird_cli/lib/src/commands/init_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ class InitCommand extends ShorebirdCommand

@override
Future<int> run() async {
final session = auth.currentSession;
if (session == null) {
if (auth.credentials == null) {
logger.err('You must be logged in.');
return ExitCode.noUser.code;
}
Expand Down Expand Up @@ -56,7 +55,7 @@ Please make sure you are running "shorebird init" from the root of your Flutter

if (shorebirdYaml != null) {
final codePushClient = buildCodePushClient(
apiKey: session.apiKey,
httpClient: auth.client,
hostedUri: hostedUri,
);

Expand Down
25 changes: 15 additions & 10 deletions packages/shorebird_cli/lib/src/commands/login_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,32 +18,37 @@ class LoginCommand extends ShorebirdCommand {

@override
Future<int> run() async {
final session = auth.currentSession;
if (session != null) {
final credentials = auth.credentials;
if (credentials != null) {
logger
..info('You are already logged in.')
..info("Run 'shorebird logout' to log out and try again.");
return ExitCode.success.code;
}

final apiKey = logger.prompt(
'${lightGreen.wrap('?')} Please enter your API Key:',
);
final loginProgress = logger.progress('Logging into shorebird.dev');
try {
auth.login(apiKey: apiKey);
loginProgress.complete();
await auth.login(prompt);
logger.info('''
🎉 ${lightGreen.wrap('Welcome to Shorebird! You are now logged in.')}
🔑 Credentials are stored in ${lightCyan.wrap(auth.sessionFilePath)}.
🔑 Credentials are stored in ${lightCyan.wrap(auth.credentialsFilePath)}.
🚪 To logout use: "${lightCyan.wrap('shorebird logout')}".''');
return ExitCode.success.code;
} catch (error) {
loginProgress.fail();
logger.err(error.toString());
return ExitCode.software.code;
}
}

void prompt(String url) {
logger.info('''
The Shorebird CLI needs your authorization to manage apps, releases, and patches on your behalf.
In a browser, visit this URL to log in:
${styleBold.wrap(styleUnderlined.wrap(lightCyan.wrap(url)))}
Waiting for your authorization...''');
}
}
3 changes: 1 addition & 2 deletions packages/shorebird_cli/lib/src/commands/logout_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ class LogoutCommand extends ShorebirdCommand {

@override
Future<int> run() async {
final session = auth.currentSession;
if (session == null) {
if (auth.credentials == null) {
logger.info('You are already logged out.');
return ExitCode.success.code;
}
Expand Down
5 changes: 2 additions & 3 deletions packages/shorebird_cli/lib/src/commands/patch_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,7 @@ class PatchCommand extends ShorebirdCommand
return ExitCode.config.code;
}

final session = auth.currentSession;
if (session == null) {
if (auth.credentials == null) {
logger.err('You must be logged in to publish.');
return ExitCode.noUser.code;
}
Expand Down Expand Up @@ -147,7 +146,7 @@ class PatchCommand extends ShorebirdCommand
final pubspecYaml = getPubspecYaml()!;
final shorebirdYaml = getShorebirdYaml()!;
final codePushClient = buildCodePushClient(
apiKey: session.apiKey,
httpClient: auth.client,
hostedUri: hostedUri,
);
final version = pubspecYaml.version!;
Expand Down
5 changes: 2 additions & 3 deletions packages/shorebird_cli/lib/src/commands/release_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,7 @@ make smaller updates to your app.
return ExitCode.config.code;
}

final session = auth.currentSession;
if (session == null) {
if (auth.credentials == null) {
logger.err('You must be logged in to release.');
return ExitCode.noUser.code;
}
Expand Down Expand Up @@ -117,7 +116,7 @@ make smaller updates to your app.
final pubspecYaml = getPubspecYaml()!;
final shorebirdYaml = getShorebirdYaml()!;
final codePushClient = buildCodePushClient(
apiKey: session.apiKey,
httpClient: auth.client,
hostedUri: hostedUri,
);
final version = pubspecYaml.version!;
Expand Down
2 changes: 1 addition & 1 deletion packages/shorebird_cli/lib/src/commands/run_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class RunCommand extends ShorebirdCommand

@override
Future<int> run() async {
if (auth.currentSession == null) {
if (auth.credentials == null) {
logger
..err('You must be logged in to run.')
..err("Run 'shorebird login' to log in and try again.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ mixin ShorebirdCreateAppMixin on ShorebirdConfigMixin {
}

final client = buildCodePushClient(
apiKey: auth.currentSession!.apiKey,
httpClient: auth.client,
hostedUri: hostedUri,
);

Expand Down
2 changes: 1 addition & 1 deletion packages/shorebird_cli/lib/src/shorebird_engine_mixin.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ mixin ShorebirdEngineMixin on ShorebirdConfigMixin {
final engineArchivePath = p.join(tempDir.path, 'engine.zip');
try {
final codePushClient = buildCodePushClient(
apiKey: auth.currentSession!.apiKey,
httpClient: auth.client,
hostedUri: hostedUri,
);
await _downloadShorebirdEngine(codePushClient, engineArchivePath);
Expand Down
Loading

0 comments on commit efdb675

Please sign in to comment.