feat: better handling of errors when refreshing the token fails (#2244)

This commit is contained in:
Erick
2024-06-14 13:56:03 -03:00
committed by GitHub
parent 6bf8c86357
commit 06cc911be5
3 changed files with 140 additions and 6 deletions
+30 -2
View File
@@ -9,6 +9,7 @@ import 'package:googleapis_auth/auth_io.dart' as oauth2;
import 'package:googleapis_auth/googleapis_auth.dart';
import 'package:http/http.dart' as http;
import 'package:jwt/jwt.dart';
import 'package:mason_logger/mason_logger.dart';
import 'package:path/path.dart' as p;
import 'package:scoped_deps/scoped_deps.dart';
import 'package:shorebird_cli/src/auth/ci_token.dart';
@@ -18,6 +19,7 @@ import 'package:shorebird_cli/src/logger.dart';
import 'package:shorebird_cli/src/platform.dart';
import 'package:shorebird_cli/src/shorebird_cli_command_runner.dart';
import 'package:shorebird_cli/src/shorebird_command.dart';
import 'package:shorebird_cli/src/third_party/flutter_tools/lib/flutter_tools.dart';
import 'package:shorebird_code_push_client/shorebird_code_push_client.dart';
export 'ci_token.dart';
@@ -115,7 +117,7 @@ class AuthenticatedClient extends http.BaseClient {
if (credentials == null) {
final token = _token!;
credentials = _credentials = await _refreshCredentials(
credentials = _credentials = await _tryRefreshCredentials(
token.authProvider.clientId,
oauth2.AccessCredentials(
// This isn't relevant for a refresh operation.
@@ -133,7 +135,7 @@ class AuthenticatedClient extends http.BaseClient {
final jwt = Jwt.parse(credentials.idToken!);
final authProvider = jwt.authProvider;
credentials = _credentials = await _refreshCredentials(
credentials = _credentials = await _tryRefreshCredentials(
authProvider.clientId,
credentials,
_baseClient,
@@ -146,6 +148,32 @@ class AuthenticatedClient extends http.BaseClient {
request.headers['Authorization'] = 'Bearer $token';
return _baseClient.send(request);
}
Future<oauth2.AccessCredentials> _tryRefreshCredentials(
oauth2.ClientId clientId,
oauth2.AccessCredentials credentials,
http.Client client, {
required oauth2.AuthEndpoints authEndpoints,
}) async {
try {
return await _refreshCredentials(
clientId,
credentials,
client,
authEndpoints: authEndpoints,
);
} catch (e, s) {
logger
..err('Failed to refresh credentials.')
..info(
'''Try logging out with ${lightBlue.wrap('shorebird logout')} and logging in again.''',
)
..detail(e.toString())
..detail(s.toString());
throw ProcessExit(ExitCode.software.code);
}
}
}
/// An OAuth 2.0 authentication provider.
+4 -4
View File
@@ -202,10 +202,10 @@ packages:
dependency: "direct main"
description:
name: collection
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
url: "https://pub.dev"
source: hosted
version: "1.18.0"
version: "1.19.0"
convert:
dependency: transitive
description:
@@ -791,10 +791,10 @@ packages:
dependency: transitive
description:
name: vm_service
sha256: "360c4271613beb44db559547d02f8b0dc044741d0eeb9aa6ccdb47e8ec54c63a"
sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc
url: "https://pub.dev"
source: hosted
version: "14.2.3"
version: "14.2.4"
watcher:
dependency: transitive
description:
@@ -6,6 +6,7 @@ import 'package:googleapis_auth/auth_io.dart';
import 'package:googleapis_auth/googleapis_auth.dart' as oauth2;
import 'package:http/http.dart' as http;
import 'package:jwt/jwt.dart' show Jwt, JwtPayload;
import 'package:mason_logger/mason_logger.dart';
import 'package:mocktail/mocktail.dart';
import 'package:path/path.dart' as p;
import 'package:platform/platform.dart';
@@ -19,6 +20,7 @@ import 'package:shorebird_code_push_client/shorebird_code_push_client.dart';
import 'package:test/test.dart';
import '../fakes.dart';
import '../matchers.dart';
import '../mocks.dart';
const googleJwtIssuer = 'https://accounts.google.com';
@@ -266,6 +268,52 @@ void main() {
expect(request.headers['Authorization'], equals('Bearer $idToken'));
});
group('when refreshing the token fails', () {
late AuthenticatedClient client;
setUp(() {
when(() => httpClient.send(any())).thenAnswer(
(_) async => http.StreamedResponse(
const Stream.empty(),
HttpStatus.badRequest,
),
);
final onRefreshCredentialsCalls = <oauth2.AccessCredentials>[];
client = AuthenticatedClient.token(
token: ciToken,
httpClient: httpClient,
onRefreshCredentials: onRefreshCredentialsCalls.add,
refreshCredentials: (
clientId,
credentials,
client, {
AuthEndpoints authEndpoints = const GoogleAuthEndpoints(),
}) async =>
throw Exception('error.'),
);
});
test('exits and logs correctly', () async {
await expectLater(
() => runWithOverrides(
() => client.get(Uri.parse('https://example.com')),
),
exitsWithCode(ExitCode.software),
);
verify(() => logger.err('Failed to refresh credentials.'))
.called(1);
verify(
() => logger.info(
'''Try logging out with ${lightBlue.wrap('shorebird logout')} and logging in again.''',
),
).called(1);
verify(
() => logger.detail('Exception: error.'),
).called(1);
});
});
test('uses valid token when credentials valid.', () async {
when(() => httpClient.send(any())).thenAnswer(
(_) async => http.StreamedResponse(
@@ -358,6 +406,64 @@ void main() {
expect(request.headers['Authorization'], equals('Bearer $idToken'));
});
group('when refreshing the token fails', () {
late AuthenticatedClient client;
setUp(() {
when(() => httpClient.send(any())).thenAnswer(
(_) async => http.StreamedResponse(
const Stream.empty(),
HttpStatus.badRequest,
),
);
const expiredIdToken =
'''eyJhbGciOiJIUzI1NiIsImtpZCI6IjEyMzQiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI1MjMzMDIyMzMyOTMtZWlhNWFudG0wdGd2ZWsyNDB0NDZvcmN0a3RpYWJyZWsuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI1MjMzMDIyMzMyOTMtZWlhNWFudG0wdGd2ZWsyNDB0NDZvcmN0a3RpYWJyZWsuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMjM0NSIsImhkIjoic2hvcmViaXJkLmRldiIsImVtYWlsIjoidGVzdEBlbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiaWF0IjoxMjM0LCJleHAiOjY3ODl9.MYbITALvKsGYTYjw1o7AQ0ObkqRWVBSr9cFYJrvA46g''';
final onRefreshCredentialsCalls = <oauth2.AccessCredentials>[];
final expiredCredentials = oauth2.AccessCredentials(
oauth2.AccessToken(
'Bearer',
'accessToken',
DateTime.now().subtract(const Duration(minutes: 1)).toUtc(),
),
'',
[],
idToken: expiredIdToken,
);
client = AuthenticatedClient.credentials(
credentials: expiredCredentials,
httpClient: httpClient,
onRefreshCredentials: onRefreshCredentialsCalls.add,
refreshCredentials: (
clientId,
credentials,
client, {
AuthEndpoints authEndpoints = const GoogleAuthEndpoints(),
}) async =>
throw Exception('error.'),
);
});
test('exits and logs correctly', () async {
await expectLater(
() => runWithOverrides(
() => client.get(Uri.parse('https://example.com')),
),
exitsWithCode(ExitCode.software),
);
verify(() => logger.err('Failed to refresh credentials.'))
.called(1);
verify(
() => logger.info(
'''Try logging out with ${lightBlue.wrap('shorebird logout')} and logging in again.''',
),
).called(1);
verify(
() => logger.detail('Exception: error.'),
).called(1);
});
});
test('uses valid token when credentials valid.', () async {
when(() => httpClient.send(any())).thenAnswer(
(_) async => http.StreamedResponse(