feat: better handling of errors when refreshing the token fails (#2244)
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user